해당 포스트는 "안드로이드 비동기 프로그래밍" 책의 내용을 요약한 것이다.



※ AsyncTask 

: AsyncTask는 추상클래스로써 상속 받은 하위 클래스는 다음의 메서드를 재정의 할 수 있다.

- protected void onPreExecute() : AsyncTask.execute()를 호출하면 바로 실행되는 메서드다. 내부 작업을 완료하면 doInBackground가 호출된다.

- protected Result doInBackground(Params... params) : 백그라운드 작업을 한다.

- protected void onProgressUpdate(Progress... values) : doInBackground 메서드에서 publishProgress() 메서드를 호출하면 onProgressUpdate가 호출되 UI를 주기적으로 바꿀 수 있다.

- protected void onPostExecute(Result result) : doInBackground에서 백그라운드 작업이 완료되면 호출되고 백그라운드 처리 결과가 result 값으로 전달되고 UI를 결과값으로 바꿀 수 있다.

- protected void onCancelled(Result result) : doInBackground가 끝나기 전에 AsyncTask의 cancel 메서드를 호출하면 호출되는 메서드로 이 때는 onPostExecute가 호출되지 않는다.


여기서 doInBackground와 publishProgress 는 백그라운드에서 실행되고 나머지 재정의 메서드들은 메인 스레드에서 실행된다. 따라서 onPreExecute(), onProgressUpdate(), onPostExecute()에서 UI를 변경할 수 있다. 


- AsyncTask 타입 선언

abstract class AsyncTask<Params, Progress, Result>

: 위 파라미터는 doInBackground에 "Params" 파라미터를 전달하고 진행 관련 메서드에 "Progress" 타입을 전달하고 결과 값으로 "Result" 타입을 반환한다는 걸 의미한다. 참고로 Void는 void를 나타내는 인스턴스화할 수 없는 클래스이다. 



ex) 해당 예는 안드로이드 비동기 프로그래밍 책에 있다.

public class Example4Activity extends Activity { private PrimesTask task; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ch2_example_layout); ((TextView)findViewById(R.id.title)).setText(R.string.ch2_ex4); ((TextView)findViewById(R.id.description)).setText(R.string.ch2_ex4_desc); Button goButton = (Button) findViewById(R.id.go); final TextView resultView = (TextView) findViewById(R.id.result); goButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { task = new PrimesTask(Example4Activity.this, resultView); task.execute(500); } }); } @Override protected void onPause() { super.onPause(); if (task != null) task.cancel(false); } }

AyyncTask는 위에 나오는 방식으로 실행할 수 있다. 또한 execute() 메서드가 AsyncTask 객체를 반환하기 때문에 다음과 같이 해도 된다.
PrimesTask task = new PrimesTask(Example4Activity.this, resultView).execute(500);
ddd

public class PrimesTask extends AsyncTask<Integer, Integer, BigInteger> { private Context ctx; private ProgressDialog progress; private TextView resultView; public PrimesTask(Context ctx, TextView resultView) { this.ctx = ctx; this.resultView = resultView; } //백그라운드가 실행되기 전에 사용자와 상호작용할 수 있도록 다이얼로그를 보여준다. @Override protected void onPreExecute() { progress = new ProgressDialog(ctx); progress.setTitle(R.string.calculating); progress.setCancelable(true); progress.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { PrimesTask.this.cancel(false); } }); progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progress.setProgress(0); progress.setMax(100); progress.show(); } @Override protected BigInteger doInBackground(Integer... params) { int primeToFind = params[0]; BigInteger prime = new BigInteger("2"); for (int i=0; i<primeToFind; i++) { prime = prime.nextProbablePrime(); int percentComplete = (int)((i * 100f)/primeToFind); publishProgress(percentComplete); //다이얼로그 값을 갱신하기 위해 호출한다. if (isCancelled()) break; } return prime; } //다이얼로그 값을 실제로 갱신한다. @Override protected void onProgressUpdate(Integer... values) { progress.setProgress(values[0]); } //백그라운드가 끝난 후 결과값을 UI에 보여주고 사용자와 상호작용했던 다이얼로그를 종료한다. @Override protected void onPostExecute(BigInteger result) { resultView.setText(result.toString()); progress.dismiss(); } @Override protected void onCancelled(BigInteger result) { resultView.setText("cancelled at " + result.toString()); progress.dismiss(); } }

진행 상황을 보여주기 위해 호출되는 publishProgress는 메인 스레드에서 직접 호출하지 못하지만 메인 스레드에서 가까운 미레에 언젠가 처리하는 메인 스레드의 큐에 작업을 추가한다. publishProgress와 onProgressUpdate는 사용자 인터페이스를 지속적으로 갱신하기 때문에 해당 메서드를 지연을 일으키지 않고 빨리 처리해야 한다. 
백그라운드 작업을 취소하려면 cancel() 메서드를 호출하면 된다. cancel() 메서드 인자로 true 값이 들어가면 백그라운드 작업을 곧바로 취소하고 false 값이 들어가면 그렇지 않다. cancel을 간단하게 호출하는 걸로 작업을 중간에 끝는 거는 불안정하다. 따라서 cancel 메서드 인자로 false를 주고 doInBackground에서 isCancelled() 메서드를 통해 주기적으로 확인함으로써 취소해야 한다.

- 예외 처리
: 메인 스레드에서 실행하는 콜백 메서드 onPreExecute(), onProgressUpdate(), onPostExecute(), onCancelled()의 경우 예외를 잡고 사용자에게 경고하기 위해 사용자 인터페이스를 직접 갱신할 수 있다. 하지만 doInBackground는 예외가 발생할 시 사용자 인터페이스를 갱신할 수 없다. 이에 대한 해결책으로 doBackground의 예외와 결과값을 포함하는 객체를 반환하는 것이다. 다음 코드를 보면 알 수 있다.
public abstract class SafeAsyncTask<R>
extends AsyncTask<Void, Void, SafeAsyncTask.Result<R>> {

    static class Result<T> {
        private T result;
        private Exception exc;
    }

    protected abstract R doSafelyInBackground(Void... params) throws Exception;

    @Override
    protected final Result<R> doInBackground(Void... params) {
        Result<R> result = new Result<R>();
        try {
            result.result = doSafelyInBackground(params);
        } catch (Exception exc) {
            result.exc = exc;
        }
        return result;
    }

    @Override
    protected final void onPostExecute(Result<R> result) {
        if (result.exc != null) {
            onCompletedWithException(result.exc);
        } else {
            onCompletedWithResult(result.result);
        }
    }

    @Override
    protected final void onCancelled(Result<R> result) {
        onCancelledWithResult(result.result);
    }

    // override me if you want to handle exceptions
    protected void onCompletedWithException(Exception exc) {
        Log.e(LaunchActivity.TAG, exc.getMessage(), exc);
    }

    // override me if you want to handle the result
    protected void onCompletedWithResult(R result) {
    }

    // override me if you to handle the partial result
    // but _don't_ call super if you override
    protected void onCancelledWithResult(R result) {
        onCancelled();
    }
}



- 멀티 스레드 지원
: API 레벨 11부터 기본적으로 여러 개의 AsyncTask를 백그라운드 단일 스레드를 통해 순차적으로 실행한다. 개발자는 다음의 메서드를 통해 Executor 객체를 이용해 AsyncTask 멀티 스레드가 가능하다.   

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params)

exec에는 AsyncTask.SERIAL_EXECUTOR, AsyncTask.THREAD_POOL_EXECUTOR, Executor 객체가 들어갈 수 있다. SERIAL_EXECUTOR는 작업을 큐에 담고 순차적으로 실행되는 단일 백그라운드 스레드를 사용한다. THREAD_POOL은 스레드 풀을 사용해 작업한다. 최소 5개의 스레드를 유지하고 128개까지 확장할 수 있다. 또한 다음과 같이 Executor 객체를 인자로 줄 수 있다.

    private static final Queue<Runnable> QUEUE 
               = new LinkedBlockingQueue<Runnable>();
    public static final Executor MY_EXECUTOR
               = new ThreadPoolExecutor(4,8,1,TimeUnit.MINUTES,QUEUE);
    task.executeOnExecutor(MY_EXECUTOR, params);


- 액티비티 생명주기 문제
: 메인 스레드가 백그라운드 작업을 완료하기 전에 액티비티가 종료(방향 전환에 따를 액티비티 재시작)되면 문제가 발생한다. 액티비티가 종료한 후 백그라운드 작업을 한다면 불필요한 작업일 수도 있고 AsyncTask가 액티비티와 뷰 계층의 일부를 참조한다면 많은 양의 메모리가 누수된다. 흔히 AsyncTask를 사용할 때 액티비티의 익명 내부 클래스로 선언해서 만드는데 액티비티가 종료되어도 백그라운드로 남아있기 때문에 액티비티에 대한 참조를 생성하여 더 큰 메모리 누수를 만든다. 이를 해결할 방법은 두 가지가 있다.

1. 이른 취소
: 액티비티를 종료했을 때 실행 중인 작업을 취소 할 수 있다. onPause가 액티비티 종료 전에 호출함이 보장되기 때문에 다음과 같이 할 수 있다.
    protected void onPause(){
    	super.onPause();
    	if((task != null) && (isFinishing()))
    		task.cancel(false);
    }

위와 같이 하면 다른 액티비티로 전환되어 해당 액티비티가 스택에 있을 때 백그라운드 작업을 완료할 수도 있다.


2. retained headless fragment의 사용

: 만약 구성 변경 때문에 액티비티가 종료돼도 백그라운드 작업을 계속 하고 재시작한 액티비티에서 결과를 출력하려면 retained headless fragment를 사용하면 된다. retained는 액티비티가 재시작해도 프래그먼트가 그대로 유지된다는 뜻이다. 프래그먼트에서 setRetainInstance(true)를 호출하면 된다. 그러면 프래그먼트 생명주기에서 onDestroy()와 onCreate() 메서드가 호출 안 되 프래그먼트가 유지된다. 이를 통해 백그라운드 작업을 계속 유지할 수 있다. headless는 프래그먼트가 액티비티의 뷰를 관리하지 않는다는 뜻이다. 즉, 액티비티 뷰를 참조하지 않는다. 따라서 AsyncTask는 프래그먼트에서 분리되어야 하고 AsyncTask는 사용자 인터페이스와 직접 상호작용하지 않아 뷰 계층 객체에 대한 참조에 대해 누수할 가능성이 적다. 다음은 retained headless fragment 예제이다.

public interface AsyncListener<Progress, Result> {
    void onPreExecute();
    void onProgressUpdate(Progress... progress);
    void onPostExecute(Result result);
    void onCancelled(Result result);
}

액티비티에서 위 인터페이스를 구현해야 한다. 그래야 fragment에서 분리된다.

public class Example5Activity extends FragmentActivity implements AsyncListener<Integer, BigInteger> { public static final String PRIMES = "primes"; private ProgressDialog dialog; private TextView resultView; private Button goButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ch2_example_layout); ((TextView)findViewById(R.id.title)).setText(R.string.ch2_ex5); ((TextView)findViewById(R.id.description)).setText(R.string.ch2_ex5_desc); resultView = (TextView)findViewById(R.id.result); goButton = (Button)findViewById(R.id.go); goButton.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { PrimesFragment primes = (PrimesFragment) getSupportFragmentManager().findFragmentByTag(PRIMES); //프래그먼트가 있는지 확인해야 한다.(retained fragment이기 때문) if (primes == null) { primes = new PrimesFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.add(primes, PRIMES); transaction.commit(); } } }); } public void onPreExecute() { onProgressUpdate(0); } public void onProgressUpdate(Integer... progress) { if (dialog == null) { prepareProgressDialog(); } dialog.setProgress(progress[0]); } public void onPostExecute(BigInteger result) { resultView.setText(result.toString()); cleanUp(); } public void onCancelled(BigInteger result) { resultView.setText("cancelled at " + result); cleanUp(); } private void prepareProgressDialog() { dialog = new ProgressDialog(this); dialog.setTitle(R.string.calculating); dialog.setCancelable(true); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { PrimesFragment primes = (PrimesFragment) getSupportFragmentManager().findFragmentByTag(PRIMES); primes.cancel(); } }); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setProgress(0); dialog.setMax(100); dialog.show(); } private void cleanUp() { dialog.dismiss(); dialog = null; FragmentManager fm = getSupportFragmentManager(); PrimesFragment primes = (PrimesFragment) fm.findFragmentByTag(PRIMES); fm.beginTransaction().remove(primes).commit(); } }


package com.packt.asyncandroid.chapter2.example5;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;

import java.math.BigInteger;

public class PrimesFragment extends Fragment {

    private AsyncListener<Integer,BigInteger> listener;
    private PrimesTask task;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        task = new PrimesTask();
        task.execute(2000);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        listener = (AsyncListener<Integer,BigInteger>)activity;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    public void cancel() {
        task.cancel(false);
    }

    class PrimesTask extends AsyncTask<Integer, Integer, BigInteger> {
        @Override
        protected void onPreExecute() {
            if (listener != null) listener.onPreExecute();
        }

        @Override
        protected BigInteger doInBackground(Integer... params) {
            int primeToFind = params[0];
            BigInteger prime = new BigInteger("2");
            for (int i=0; i<primeToFind; i++) {
                prime = prime.nextProbablePrime();

                int percentComplete = (int)((i * 100f)/primeToFind);
                publishProgress(percentComplete);

                if (isCancelled())
                    break;
            }
            return prime;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            if (listener != null) listener.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(BigInteger result) {
            if (listener != null) listener.onPostExecute(result);
        }

        @Override
        protected void onCancelled(BigInteger result) {
            if (listener != null) listener.onCancelled(result);
        }
    }
}
onAttach와 onDetach에서 액티비티에 activity에 대한 참조를 추가하고 제거해야 한다.



- AsyncTask 응용
: AsyncTask는 많아야 1~2초 정도 연산하는 빨리 끝나는 작업이 이루어져야 한다. 짧은 실행과 대량의 고속처리나 큰 텍스트 문자열에서의 단어 탐색과 같은 CPU 집약적인 작업에 이상적이다. 텍스트 파일을 읽고 쓰거나 BitmapFactory를 이용해 로컬 파일로부터 이미지 불러오기 같은 I/O도 AysncTask를 이용한 훌륭한 사례다. 단, 1~2초 이상을 요구하는 작업은 절대로 해서는 안되고 특히 retained headless fragment를 사용할 시 복잡도가 증가하므로 더 주의해야 한다. 또한 네트워크 I/O를 수행하는 것도 추천하지 않는다.



+ Recent posts