반응형

본 글은 안드로이드에서 AsyncTsak의 개념 및 사용법에 대해 설명합니다.



동기(sync)와 비동기(async)

동기적으로 태스크를 실행한 후,  다른 태스크를 실행하려면 먼저 실행된 태스크가 종료되기를 기다려야 합니다.


비동기적으로 태스크를 실행하면  먼저 실행된 태스크가 종료되기 전에 다른 태스크를 실행할 수 있습니다.

예를 들어 메인 스레드가 실행되는 중에, 다른 스레드를 백그라운드로 실행시켜 두고 계속 메인스레드는 자신의 작업을 하다가, 이 후 백그라운드에서 돌던 스레드가 종료시 결과값을 받을 수 있습니다.


AsyncTask 개념

앱이 실행되면 안드로이드 시스템은 메인 스레드를 생성합니다. 이 스레드는 안드로이드 UI 툴키트에 접근합니다. 사용자의 입력을 기다리거나 디바이스 화면에 그리는 작업등을 다룹니다. 그래서 UI 스레드라고도 부릅니다.


앱의 모든 컴포넌트(Activity, Service, Content Provider, BroadcastReceiver 등)들은 같은 스레드내에서 실행됩니다. 필요에 따라 추가 스레드를 생성할 수 있습니다.  UI 스레드가 thread-safe하지 않기 때문에  스레드 사용시 다음 2가지를 지켜야 합니다.

  • UI 스레드가 블록(대기)되지 않도록 해야합니다.
  • UI 스레드 외에 다른 스레드에서 UI 컴포넌트 접근를 하면 안됩니다.

위에서 UI 스레드는 블록되면 안된다 했는데  문제는 안드로이드는 single thread model모델을 따르기 때문에 문제가 생깁니다. 만약 오랜 시간이 걸리는 작업을 UI 스레드에서 수행한다면  작업이 완료 될때 까지  UI 스레드가 대기해야 하므로 UI는 먹통이 됩니다. 예를 들어 다수의 파일들을 다운로드 받는 작업을 UI 스레드에서 수행하면 모든 파일의 다운로드가 완료될 때까지 UI는 반응이 없게 됩니다. 


UI 반응성 향상과 처리시간이 오래 걸리는 작업 처리를 같이 해결하기 위해선 별도의 스레드가 필요합니다. 예를 들어 네트워크 관련 처리는 메인 스레드에서 수행하는게 금지되어 있습니다.


이 문제를 해결하기 위해 안드로이드에서는 Handler, Runnavble, AsyncTask등을 제공합니다. 

AsyncTask는 메인스레드에서 생성 후 실행되며, 메인 스레드에서 처리시간이 오래 걸리는 작업을 백그라운드 스레드로 넘기고 

계속 메인 스레드 작업을 진행하기 위해 사용됩니다. AsyncTask는 위에서 설명한 비동기 태스크로써 백그라운드 스레드라는 별도의 스레드에서 작업을 수행하기 때문에  AsyncTask를 실행시켜 놓고 메인 스레드는 다음 작업을 바로 할 수 있습니다.


백그라운드 스레드는 작업 처리 중 메인 스레드에서 처리하는 UI 작업에 영향을 주지 않기 때문에 UI가 늦게 뜨거나 터치에 늦게 반응하는 등의 일이 발생하지 않습니다. 


AsyncTask를 사용하면 백그라운드 스레드와 메인 스레드간에 커뮤티케니션이 간단해집니다. 백그라운드 스레드에서 작업 종료 후, 결과를 메인 스레드에서 통보해 줄 수 있고(onPostExecute) , 또한 백그라운드 스레드에서 작업 중에도 메인 스레드에게 UI 처리 요청을 쉽게 할 수 있습니다.(onProgressUpdate )



AsyncTask와 Activity Lifecycle

수 초정도의 짧은 시간걸리는 작업에 대해서만 AsyncTask를 사용하도록 권장하고 있으며 그 이상 시간이 걸리는 작업에 대해서는 

java.util.concurrent에 포함되어 있는  Executor, ThreadPoolExecutor, FutureTask등을 사용하라고 합니다.  


그 이유에는 몇가지가 있습니다.


문제 1. 메모리 릭

 AsyncTask를 생성했던 Activity가 먼저 destroy되는 경우 메모리 릭이 발생할 수 있습니다. 왜냐면 Activity가 destory된다고 해서 AsyncTask도 같이 destory되지 않기 때문입니다.   doInBackground() 메소드가 완료될때까지 AsyncTask는 실행 중 상태를 유지하게 됩니다. 완료 후, cancel 메소드 호출된 적이 있다면 onCancelled메소드가 실행되고 그렇지 않으면  onPostExecute 메소드가 호출됩니다.


이미 자신을 실행시켰던 Activity가 존재하지 않는데 AsyncTask가 UI처리같은 걸을 요구하면 메모리상에 존재하지 않는 것을 참조하게 되어 메모리릭이 발생합니다. 따라서 반드시  Activity 종료 전 AsyncTask를 cancel해주어 백그라운드 스레드에서 처리하던 작업이 종료되도록 해야 합니다.


문제 2. 디바이스의 화면 회전(orientation)

Activity에서 AsyncTask를 실행하고나서 사용자가 디바이스의 화면을 회전시키면 Activity를 destroy를 시키고 (onDestroy)  새로운 Activity 인스턴스가 생성됩니다.(onCreate).  이때 Activity의 모든 멤버변수의 인스턴스가 새로 생성됩니다. 


Activity의 상태가 변할 때, AsyncTask에 대한 처리를 자동으로 해주지 않기 때문에,  AsyncTask는 백그라운드 스레드에서 작업을 하고 있는 상태로 남아있게 됩니다. 작업이 완료될 때까지 AsyncTask는 종료되지 않습니다. 


문제는 AsyncTask가 작업을 완료하고 Activity의 UI를 업데이트 하려 할때, 새로 생성된 Activity 인스턴스가 아닌 이미 destroy된 Activity 인스턴스에 접근하기 때문입니다.  따라서 디바이스 화면의 UI를 업데이트 할 수 없으며,  예외(IllegalArgumentException)가 발생합니다. 또한 기존 destroy된 Activity를 계속 참조하고 있기 때문에  garbage collector가 destory된 Activity를 수집할 수 없어 메모리릭이 발생할 수 있습니다.



AsyncTask의 생성 및 실행

MainActivity 클래스에서  생성한 후, AsyncTask의 execute 메소드를 호출함으로써 시작되며, 바로 AsyncTask의 onPreExecute 메소드가 호출되게 됩니다. execute 메소드의 인자로 전달한 값은 doInBackground 메소드가 파라메터로 받게 됩니다.

1
2
final DownloadFilesTask downloadTask = new DownloadFilesTask(MainActivity.this);
downloadTask.execute(fileURL);
cs



AsyncTask 병렬 실행

AsyncTask().execute();

안드로이드 3.0 허니콤(API 11)이후 단일 백그라운드 스레드에서 태스크들이 순차적으로 실행됩니다.


다수의 AsyncTask를 실행하면 doInBackground 메소드가 블록되는 상황이 발생합니다.

왜냐면 단일 스레드 상에서 실행되는 모든 AsyncTask는 순차적으로 실행되므로 

이전 태스크가 종료되기를 기다려야 합니다. 따라서 대기하지 않고 바로 실행시켜주려면 별도의 스레드에서 실행해야합니다.


이 문제를 해결하기 위해선 다음 메소드를 사용해야 합니다.

AsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)

태스크들이 독립적이 스레드에서 병렬적으로 실행됩니다.


아래 링크를 보면 위 두가지 AsyncTsask 실행방법의 결과 차이를 보여주고 있습니다.

http://www.programing-style.com/android/android-api/android-asynctask-order-execute/



안드로이드 시스템은 고정크기의 스레드 풀을 예약해두고 이미 생성된 스레드를 재사용합니다. 고정된 크기만큼의 태스크가 실행중인데 또다시 태스크가  추가 실행되면 문제가 생길 수 있습니다.



AsyncTask의 파라메터 타입

1
 private class DownloadFilesTask extends AsyncTask<URLStringLong>
cs

Params -  AsyncTask를 시작하기 위해 execute() 메소드가 호출될때 전달한 인자를 doInBackground 메소드에서 파라메터로써 전달 받게 되는데 

             이 때 사용되는 타입입니다. 

Progress - doInBackground 메소드에서 백그라운드 처리 중에 publishProgress 메소드를 호출하여 전달한 인자를 

              onProgressUpdate 메소드에서 파라메터로 받게 되는데, 이때  사용되는 타입입니다.

Result - doInBackground 메소드에서 리턴한 값을 onPostExecute 메소드에서 파라메터로써  받게 되는데,  이때 사용되는 타입입니다.



AsyncTask의 메소드

AsyncTask를 상속 받은 서브클래스에선 다음 메소드들을 오버라이딩(Override) 해주어야 합니다.  최소한 doInBackground 메소드를 오버라이딩해주어야 합니다.  오버라이딩은 상위 클래스의 메소드를 재정의해서 사용하는 것입니다.

오버라이딩된 메소드 앞에는 @override를 붙여줍니다. 이렇게 하면  컴파일러에게 정의하는 메소드가 오버라이딩을 목적으로 정의되었다는 사실을 알려줍니다. 만약 상위 클래스에 없는 메소드를 재정의했다면 에러가 나게 됩니다. 

doInBackground 메소드만 백그라운드 스레드에서 실행되며, 나머지는 메인 스레드에서 실행됩니다.


onPreExecute() 

UI 스레드상에서 실행되며 doInBackground 메소드 전에 호출됩니다.  doInBackground 메소드가 실행되기 전에  프로그레스바를 보여주는 등의 필요한 초기화 작업을 하는데 사용됩니다.


doInBackground(Params...)

이 메소드에 포함된 코드는 백그라운드 스레드 상에서 처리되며 이곳에서 UI 처리를 하면 안됩니다.  onPreExecute 메소드 종료 후, 바로 호출됩니다. 

AsyncTask의 execute 메소드를 호출시 전달한 인자를 파라메터로 받게 됩니다.  값을 리턴하면 onPostExecute 메소드에서 파라메터로 받게됩니다. 

다운로드 진행상태를 보여주는 프로그레스 다이얼로그 상태 업데이트 같은 UI 작업이 필요할 경우 ,  publishProgress 메소드에 인자로 값을 전달하여 onProgressUpdate메소드에서 파라메터로 받은 값을 가지고 UI 작업을 하도록 합니다.


onProgressUpdate(Progress...) 

doInBackground메소드에서 publishProgress 메소드를 호출함으로써 UI 스레드상에서 실행됩니다.  백그라운드 스레드에서 작업 처리 중에 프로그레스바 진행 상태 업데이트 같은  UI작업이 필요한 경우,  publishProgress 호출해서 onProgressUpdate 메소드에서 UI작업을 하도록 합니다.  publishProgress  메소드에서 인자로 전달한 값을 파라메터로 받아서 UI 작업시 사용합니다.


onPostExecute(Result)

UI 스레드 상에  실행되며, doInBackground 메소드 종료 후  호출됩니다. doInBackground 메소드에서 리턴한 값을 onPostExecute 메소드에서 파라메터로 전달받습니다. doInBackground에서 작업하는 동안 보여주던 프로그레스바를 감추는 작업같은 UI 작업을 할 수 있습니다.


onCancelled()

doInBackground 메소드에서 수행중인 작업이 취소되면  onPostExecute 메소드 대신에 호출됩니다.



보통 백그라운드 스레드의 처리 진행상황을 보여주기 위해 onProgressUpdate 메소드가 사용되며  백그라운드 스레드의 처리 결과를 한번에 받기 위해서 onPostExecute 메소드가 사용됩니다.



작업 중 취소 처리 (Task Cancellation)

doInBackground 메소드에서 백그라운드 작업을 처리하는 중에  태스크는 cancel 메소드 호출에 의해서 취소될 수 있습니다. 그 결과  isCancelled 메소드가 true를 반환하게 되며  doInBackground 메소드 리턴 후, onPostExecute 메소드 대신 onCancelled메소드가 호출됩니다. 태스크 작업 취소 요청에 바로 반응하기 위해서는 doInBackground 메소드에서 주기적으로 isCancelled 메소드의 리턴값을 체크하고 있어야 합니다. 


AsyncTask가 따르는 Threading rules

  • AsyncTask 클래스는 메인 스레드에서 생성 및 호출되어야 합니다.  execute 메소드는 메인 스레드에서 실행되어야 합니다.
  • AsyncTask를 생성 후,  실행은 한번만 가능합니다.  또 다시 호출하면 예외가 발생합니다.
  • 직접 onPreExecute, onPostExecute, doInBackground, onProgressUpdate를 호출할 수 없습니다.
  • 하나의 액티비티에서 두 개 이상의 AsyncTask가 실행되면 순차적을 실행됩니다. 이를 극복하기 위해 executeOnExecutor 메소드가 제공됩니다.


AsyncTask 예제

전체 소스코드 및 실행 시나리오는 다음 포스팅에 포함되어 있습니다. AsyncTask관련 코드만 설명하고 부가적인 코드들은 추후에 다른 포스팅에서 설명하도록 하겠습니다.

[Android/예제 프로젝트] - Android 예제 - URL 주소로 부터 동영상 다운로드 및 재생( AsyncTask, URLConnection, PowerManager )


1. onCreate메소드를 보면 버튼 클릭시 AsyncTask를 생성한 후, execute 메소드를 호출하여 실행시킵니다.  AsyncTask는 비동기 태스크라서 메인스레드는 AsyncTask가 종료되기를 기다리지 않고 바로 다음 코드를 수행하게 됩니다. 

execute 메소드 호출시 인자로 fileURL(동영상 파일의 URL 주소)을 넘겨 주고 있는데 AsyncTask의 doInBackground메소드에서 파라메터로 받게 됩니다.  

1
2
3
4
5
6
7
8
9
@Override
protected void onCreate(Bundle savedInstanceState) {
....................................................
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) { //1
            .................................
                 final DownloadFilesTask downloadTask = new DownloadFilesTask(MainActivity.this);
                 downloadTask.execute(fileURL);
cs


2. AsyncTask에서 사용하는 3개의 파라메터 타입을 이 부분에 적어줍니다. 

첫번째 String은 execute메소드에서  doInBackground 메소드로 값을 전달하는데 사용되는 타입입니다.

두번째 String은 백그라운드 스레드(doInBackground메소드)에서 publishProgress 메소드를 호출하여 onProgressUpdate메소드로 값을 전달하는 데 사용되는 타입입니다.

세번째 Long은  doInBackground메소드에서 리턴시 onPostExecute 메소드에서 파라메터로써 받을 때 사용되는 타입입니다.

1
 private class DownloadFilesTask extends AsyncTask<StringStringLong> {
cs


위에서 선언해준 파라메터 타입 세가지와 메소드의 파라메터 타입이 일치해야합니다.

1
protected Long doInBackground(String... string_url)
cs

1
protected void onProgressUpdate(String... progress) 
cs

1
protected void onPostExecute(Long size)
cs


3. AsyncTask가 실행되면 onPreExecute 메소드가 호출됩니다.  doInBackground 메소드(백그라운드 스레드)에서 작업을 시작하기 전 필요한 작업들을 해줍니다. 여기에선 다운로드 중 CPU가 잠들지 않도록 하는 처리(PowerManager.WakeLock를 acquire)와 다운로드 진행상태를 표시하기 위한 프로그레스바를 미리 화면에 보여주는 것을 하고 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
        @Override
        protected void onPreExecute() { //2
            super.onPreExecute();
 
            //사용자가 다운로드 중 파워 버튼을 누르더라도 CPU가 잠들지 않도록 해서
            //다시 파워버튼 누르면 그동안 다운로드가 진행되고 있게 됩니다.
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
            mWakeLock.acquire();
 
            //파일 다운로드를 시작하기 전에 프로그레스바를 화면에 보여줍니다.
            progressBar.show();
        }
cs


4. 백그라운드 스레드에서 파일 다운로드를 진행합니다. URLConnection를 사용하여 파라메터로 받은 URL 주소와 연결한 후, InputStream으로 데이터를 가져오며 SD카드의 특정 경로를 File객체를 생성하여 OutputStream으로 데이터를 기록합니다. 

다운로드 중 작업 취소 요청이 있는지를 isCancelled()로 검사하며, publishProgress 메소드를 호출시 인자로 전체파일 크기중 몇 퍼센트 받았는지를 인자로 전달하여 onProgressUpdate 메소드에서 파라메터로써 받은 값을 가지고 다운로드 진행 상황을 프로그레스 다이얼로그에 업데이트 하도록합니다.

다운로드 완료 후, PowerManager.WakeLock를 release 해주며 마지막으로 다운로드 받은 파일 크기를 리턴합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
        //파일 다운로드를 진행합니다.
        @Override
        protected Long doInBackground(String... string_url) { //3
            int count;
            long FileSize = -1;
            InputStream input = null;
            OutputStream output = null;
            URLConnection connection = null;
 
            try {
                URL url = new URL(string_url[0]);
                connection = url.openConnection();
                connection.connect();
 
 
                //파일 크기를 가져옴
                FileSize = connection.getContentLength();
 
                //URL 주소로부터 파일다운로드하기 위한 input stream
                input = new BufferedInputStream(url.openStream(), 8192);
 
                path= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                outputFile= new File(path, "Alight.avi"); //파일명까지 포함함 경로의 File 객체 생성
 
                // SD카드에 저장하기 위한 Output stream
                output = new FileOutputStream(outputFile);
 
 
                byte data[] = new byte[1024];
                long downloadedSize = 0;
                while ((count = input.read(data)) != -1) {
                    //사용자가 BACK 버튼 누르면 취소가능
                    if (isCancelled()) {
                        input.close();
                        return Long.valueOf(-1);
                    }
 
                    downloadedSize += count;
 
                    if (FileSize > 0) {
                        float per = ((float)downloadedSize/FileSize) * 100;
                        String str = "Downloaded " + downloadedSize + "KB / " + FileSize + "KB (" + (int)per + "%)";
                        publishProgress("" + (int) ((downloadedSize * 100/ FileSize), str);
 
                    }
 
                    //파일에 데이터를 기록합니다.
                    output.write(data, 0, count);
                }
                // Flush output
                output.flush();
 
                // Close streams
                output.close();
                input.close();
 
 
            } catch (Exception e) {
                Log.e("Error: ", e.getMessage());
            }finally {
                try {
                    if (output != null)
                        output.close();
                    if (input != null)
                        input.close();
                } catch (IOException ignored) {
                }
 
                mWakeLock.release();
 
            }
            return FileSize;
        }
cs


5. doInBackground 메소드(백그라운드 스레드)에서 publishProgress 메소드 호출시 전달한 인자를 파라메터로써 받아서 프로그레스 다이얼로그에 몇퍼센트 받았는지 출력 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
        //다운로드 중 프로그레스바 업데이트
        @Override
       protected void onProgressUpdate(String... progress) { //4
            super.onProgressUpdate(progress);
 
            // if we get here, length is known, now set indeterminate to false
            progressBar.setIndeterminate(false);
            progressBar.setMax(100);
            progressBar.setProgress(Integer.parseInt(progress[0]));
            progressBar.setMessage(progress[1]);
        }
cs


6. 다운로드가 완료되어 doInBackground 메소드(백그라운드 스레드)가 리턴되면 호출되며, 파라메터로 파일크기를 받았습니다. 

프로그래스 다이얼로그를 안보이도록 하고, Intent를 사용하여 다운로드 받은 파일을 설치된 동영상 플레이 앱을 이용하여 플레이하도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        //파일 다운로드 완료 후
        @Override
        protected void onPostExecute(Long size) { //5
            super.onPostExecute(size);
 
            progressBar.dismiss();
 
            if ( size > 0) {
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다. 파일 크기=" + size.toString(), Toast.LENGTH_LONG).show();
 
                Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                mediaScanIntent.setData(Uri.fromFile(outputFile));
                sendBroadcast(mediaScanIntent);
 
                playVideo(outputFile.getPath());
 
            }
            else
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
        }
cs



화면 회전시 프로그레스바가 사라지는 문제에 대한 설명과 해결방법은 다음 포스팅을 참고하세요..


[Android/개념 및 예제] - 안드로이드 개념 및 예제 - 화면 회전시 AsyncTask에서 ProgressBar 처리 방법 ( Fragment 이용 )



문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts