ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Android AsyncTask
    안드로이드 2020. 5. 10. 12:20
    728x90

    지난 글에서 안드로이드의 메인 스레드와 작업 스레드의 역할과 통신에 대해서 알아봤다면 이번에는 이를 편하게 해주는 Helper Class 중 하나인 AsyncTask 에 대해서 알아볼거에요

     

    메소드

    메인스레드에서 동작하는 메소드

    onPreExecute()

    onProgressUpdate()

    onCancelled()

    onPostExecute()

     

    작업스레드에서 동작하는 메소드

    doInBackground()

    AsyncTask 구조도

    onPreExecute()

    작업 시작 전에 실행되는 메소드로, 주로 변수 초기화 작업이 이루어집니다.

     

    doInBackground()

    작업 스레드가 실행시키는 메소드로 Looping 기능이 없어서 자체적으로 무한 루프를 돌게 만들어야 합니다. 리턴되면 작업 스레드도 같이 사라집니다.

     

    onProgressUpdate()

    작업 스레드가 하고 있는 일의 진척도를 레이아웃에 반영해야할 때 주로 사용합니다. 게임에서 리소스 파일 다운로드 할 때 몇% 완료 이런거 띄우는 기능이 여기서 이루어집니다.

     

    onCancelled()

    doInBackground() 가 종료되기 전에 단 한 번이라도 cancel() 이 호출된 적이 있다면 doInBackground() 리턴 후 실행됩니다.

     

    onPostExecute()

    doInBackground() 가 종료되기 전에 단 한 번도 cancel() 이 호출된 적이 없다면 doInBackground() 리턴 후 실행됩니다.

     

    AsyncTask 기본 구조

    AsyncTask를 만들 때 3개의 Generic 이 필요한데 각 역할은 다음과 같습니다.

    첫 번 째 Generic : doInBackground 의 작업 내용을 전달하는 용도

    두 번 째 Generic : 작업 스레드의 작업 내용을 표현하는 방식

    세 번 째 Generic : AsyncTask의 최종 결과물을 표현하는 방식

     

    예제

    <layout/activity_main.xml>

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/btn"
            android:text="버튼"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </LinearLayout>

     

    <MainActivity.java>

    package kr.co.sample;
    
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    
    import androidx.annotation.NonNull;
    import androidx.appcompat.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
    
        Button btn;
    
        private class MyTask extends AsyncTask<Integer, Integer, String> {
    
            private int num;
    
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
    
                Log.d("MyTask", "onPreExecute");
                num = 0;
            }
    
            @Override
            protected String doInBackground(Integer... integers) {
                Log.d("MyTask", "doInBackground");
    
                for (int i = 0; i < integers[0]; i++) {
                    num++;
                    
                    // onProgressUpdate() 를 호출해주는 메소드
                    publishProgress(num, integers[0]);
    
                    try {
                    	// 0.1초 대기
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                // onPostExecute("성공했습니다"); 호출
                return "성공했습니다.";
            }
    
            @Override
            protected void onProgressUpdate(Integer... values) {
                super.onProgressUpdate(values);
    
                Log.d("MyTask", "onProgressUpdate");
                
                // 몇 % 까지 진행되었는지 화면에 보여주는 용도
                int percentage = (int) ((double) values[0] / values[1] * 100);
                Log.i("MyTask",  percentage + "% 완료");
                btn.setText(percentage + "% 완료");
            }
    
            @Override
            protected void onCancelled() {
                super.onCancelled();
    
                Log.d("MyTask", "onCancelled");
            }
    
            @Override
            protected void onCancelled(String s) {
                super.onCancelled(s);
    
                Log.d("MyTask", "onCancelled " + s);
            }
    
            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
                
                // s == "성공했습니다."
                Log.d("MyTask", "onPostExecute " + s);
                btn.setText(s);
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            btn = findViewById(R.id.btn);
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    MyTask myTask = new MyTask();
                    
                    // AsyncTask 실행
                    // doInBackground([3]);
                    myTask.execute(3);
                }
            });
        }
    }
    

     

    결과

    특이한 점은 doInBackground() 메소드에서 onProgressUpdate() 를 직접 호출하면 에러가 납니다.

    이는 onProgressUpdate() 가 작업 스레드에서 호출되었기 때문이구요. publishProgress() 메소드를 호출해주면 안드로이드가 알아서 메인스레드를 통해 onProgressUpdate() 를 호출해준답니다.

    여러 개의 AsyncTask 실행해보기

    @Override
    public void onClick(View v) {
        MyTask myTask1 = new MyTask();
        MyTask myTask2 = new MyTask();
        myTask1.execute(3);
        myTask2.execute(3);
    }

     

    onClick 메소드를 위와 같이 수정하고 돌려보시면 

    작업 스레드라서 굉장히 두 AsyncTask 가 동시에 실행될 것 같지만 그렇지 않고, myTask1 이 모두 완료된 뒤에 myTask2 가 실행됨을 알 수 있습니다.

     

    여러 개의 AsyncTask 가 동시에 실행되려면 executeOnExecutor() 메소드와 AsyncTask.THREAD_POOL_EXECUTOR 옵션을 사용하시면 됩니다.

     

    MyTask myTask1 = new MyTask();
    MyTask myTask2 = new MyTask();
    myTask1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 3);
    myTask2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 3);

     

    명시적으로 순차 실행을 원하면 AsyncTask.SERIAL_EXECUTOR 옵션을 사용할 수도 있습니다.

    AsyncTask 중단하기

    package kr.co.sample;
    
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    
    import androidx.annotation.NonNull;
    import androidx.appcompat.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
    
        Button btn;
        MyTask myTask = new MyTask();
        boolean isRunning = false;
    
        private class MyTask extends AsyncTask<Integer, Integer, String> {
    
            private int num;
    
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
    
                Log.d("MyTask", "onPreExecute");
                num = 0;
                isRunning = true;
            }
    
            @Override
            protected String doInBackground(Integer... integers) {
                Log.d("MyTask", "doInBackground");
    
                for (int i = 0; i < integers[0]; i++) {
                    num++;
                    publishProgress(num, integers[0]);
    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        return "실패했습니다.";
                    }
                }
                return "성공했습니다.";
            }
    
            @Override
            protected void onProgressUpdate(Integer... values) {
                super.onProgressUpdate(values);
    
                Log.d("MyTask", "onProgressUpdate");
    
                int percentage = (int) ((double) values[0] / values[1] * 100);
                Log.i("MyTask",  percentage + "% 완료");
                btn.setText(percentage + "% 완료");
            }
    
            @Override
            protected void onCancelled() {
                super.onCancelled();
    
                Log.d("MyTask", "onCancelled");
                isRunning = false;
            }
    
            @Override
            protected void onCancelled(String s) {
                super.onCancelled(s);
    
                Log.d("MyTask", "onCancelled " + s);
                btn.setText(s);
            }
    
            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
    
                Log.d("MyTask", "onPostExecute " + s);
                btn.setText(s);
                isRunning = false;
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            btn = findViewById(R.id.btn);
    
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (!isRunning) {
                        myTask.execute(100);
                    } else {
                        myTask.cancel(true);
                    }
                }
            });
        }
    }
    

     

    myTask 를 Field 로 옮기고, isRunning 이라는 Field 도 추가했습니다. 태스크가 일을 하고 있는지 안하고 있는지 체크하기 위한 변수에요

    doInBackground() 메소드에서도 catch() 구문에 들어가게 되면

    return "실패했습니다.";

    되도록 바뀌구, onClick() 메소드에서도 두 번 클릭 하면 태스크가 취소되게 바뀌었습니다.

    결과

    doInBackground() 내부에

    try {
      ...
    } catch (InterruptException e) {

    }

    이런 구문이 있으면 myTask.cancel(true); 를 통해 doInBackground() 메소드에서 catch 문으로 빠지게 돼요

    return "실패했습니다.";

    근데 중요한 점은 위 return 구문을 추가해주지 않으면 다음과 같은 결과가 나와요

    6%에서 멈췄는데 Cancelled 의 메시지는 성공했습니다. 라고 뜨는걸 볼 수 있어요.

    이게 catch 문으로 들어간다고 doInBackground() 가 종료되는건 아니에요.

    프로그래머가 catch 문에서 적절히 종료를 시켜줘야한다는거죠

    그럼 6%에서 왜 멈췄을까요? publishProgress() 메소드는 cancel() 이 한 번이라도 호출되었다면 onProgressUpdate() 메소드를 실행시켜주지 않아요. 그래서 for 문은 열심히 돌고 있는데 onProgressUpdate()가 실행되지 않았고, 화면은 아무런 변화도 없게 되는거에요

     

    그 다음 중요한 점은 myTask.cancel() 메소드에요.

    myTask.cancel(true);     // InterruptExecption 발생
    myTask.cancel(false);    // InterruptException 미발생

    코드를 원래 상태로 바꾸고, myTask.cancel(true); 를 false 로 바꿔보세요

    이번에도 onCancelled() 에 성공했습니다. 가 떴죠?

    원래대로라면 catch 문에 들어가서 return "실패했습니다."; 가 실행되어야 하는데 cancel(false); 는 InterruptException 을 발생시키지 않기 때문에 catch 문에 들어가질 않아요.

     

    이를 해결하기 위해서는 doInBackground() 메소드를 다음과 같이 수정하면 돼요

    @Override
    protected String doInBackground(Integer... integers) {
        Log.d("MyTask", "doInBackground");
    
        for (int i = 0; i < integers[0]; i++) {
            num++;
            publishProgress(num, integers[0]);
            
            if (isCancelled())
                return "실패했습니다.";
    
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return "실패했습니다.";
            }
        }
        return "성공했습니다.";
    }

     

    isCancelled() 메소드를 통해서 태스크가 종료되었는지 확인하는거죠

    이제 잘 뜨네요 ㅎㅎ

    InterruptException 에 대해서..

    interruptException 은 쉬고 있는 스레드에 대해서만 전달되게 되어있어요.

    이게 무슨 말이냐면 wait(), sleep(), join() 메소드 같이 스레드가 WAITING 혹은 TIMED_WAITING 상태일 때만 전달이 된다는거에요

     

    package kr.co.sample;
    
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    
    import androidx.annotation.NonNull;
    import androidx.appcompat.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
    
        Button btn;
        MyTask myTask = new MyTask();
        boolean isRunning = false;
    
        private class MyTask extends AsyncTask<Integer, Integer, String> {
    
            private int num;
    
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
    
                Log.d("MyTask", "onPreExecute");
                num = 0;
                isRunning = true;
            }
    
            @Override
            protected String doInBackground(Integer... integers) {
                Log.d("MyTask", "doInBackground");
    
                for (int i = 0; i < integers[0]; i++) {
                    num++;
                    publishProgress(num, integers[0]);
    
                    try {
                    } catch (Exception e) {
                        e.printStackTrace();
                        return "실패했습니다.";
                    }
                }
                return "성공했습니다.";
            }
    
            @Override
            protected void onProgressUpdate(Integer... values) {
                super.onProgressUpdate(values);
    
                Log.d("MyTask", "onProgressUpdate");
    
                int percentage = (int) ((double) values[0] / values[1] * 100);
                Log.i("MyTask",  percentage + "% 완료");
                btn.setText(percentage + "% 완료");
            }
    
            @Override
            protected void onCancelled() {
                super.onCancelled();
    
                Log.d("MyTask", "onCancelled");
                isRunning = false;
            }
    
            @Override
            protected void onCancelled(String s) {
                super.onCancelled(s);
    
                Log.d("MyTask", "onCancelled " + s);
                btn.setText(s);
                isRunning = false;
            }
    
            @Override
            protected void onPostExecute(String s) {
                super.onPostExecute(s);
    
                Log.d("MyTask", "onPostExecute " + s);
                btn.setText(s);
                isRunning = false;
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            btn = findViewById(R.id.btn);
    
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (!isRunning) {
                        myTask.execute(10000000);
                    } else {
                        myTask.cancel(true);
                    }
                }
            });
        }
    }
    

     

    예제를 살짝 바꿔서 doInBackground() 에서 sleep(); 을 없애고 InterruptException 을 그냥 Exception 으로 바꿨어요

    그리고 isCancelled() 를 통한 탈출도 없애버렸죠. 무엇보다 sleep 없이 우리가 두 번 클릭 할 수 있는 시간동안 태스크가 끝나면 안되기에 myTask.execute(되게 큰 수); 를 넣어줬습니다. (컴퓨터의 for 문은 매우 빠르니까요...!)

    마지막으로 myTask.cancel(false); 도 true 로 바꿨어요.

    그리고 실행해보면

     

    성공했습니다. 가 리턴되는게 보이시죠?

    이게 sleep() 구문이 없이 정신 없이 돌고 있는 스레드에게는 InterruptException 이 전달되지 않기 때문이에요.

    동료가 너무 열심히 일하고 있으면 약간 방해하기 미안하잖아요? 그래서 방해하지 않다가 잠깐 쉬는 상태(wait, sleep 등) 가 되면 그제서야 방해하는 그런 느낌이에요

    '안드로이드' 카테고리의 다른 글

    안드로이드 HandlerThread  (0) 2020.05.13
    안드로이드 CountDownTimer  (0) 2020.05.13
    안드로이드 Looper, Message Queue, Handler  (0) 2020.05.09
    안드로이드 핸들러  (0) 2020.05.09
    안드로이드 멀티스레드  (0) 2020.05.09

    댓글

Designed by Tistory.