AsyncTask는 가장 많이 사용되는 비동기 task를 구현하는 안드로이드 헬퍼이지만 몇 가지 문제가 있어, 이를 보완하고, 개선하여 ExAsyncTask라는 클래스를 만들게 되었다. ExAsyncTask가 AsyncTask와 비교하여 다른 점이나, 더 개선된 점은 아래와 같다.
- ExAsyncTask는 AyncTask의 기본적인 사용법이 동일하다. 기존에 AsyncTask의 이름을 ExAsyncTask로 변경하게 되면 거의 문제없이 동작한다. (Generic parameter, getStatus, cancel, onPreExecute, doInBackground, onProgressUpdate, onPostExecute, onCancelled, get)
- ExAyncTask는 추가적으로 onException(Exception ex)과 onFinally()를 제공한다.
- ExAsyncTask는 task의 실행이 Serial하지 않다. 여러 task가 동시 실행된다.
execute()를 호출하면 이전에 실행시칸 task의 종료를 기다리지 않고 바로 실행된다. AsyncTask는 targetSdk 버전에 따라서 Serial했다가 아니었다가 했지만 최종적으로는 Serial하게 되었다. 이유가 있어서 이렇게 바뀌었겠지만 이전 task가 종료되지 않는다면 종료되기를 기다리게되고, 결국 앱이 block될 가능성이 있다. 특히 네트웍 작업을 할때 서버가 오랫동안 request의 응답을 주지 않는 경우가 있는데 이때를 대비하여 동시 실행으로 동작하는 것이 더 낫다고 본다. Serial하게동작시킬 필요가 있다면 executOnExecutor를 이용하면 된다.
- 디버깅이 용이다.
ThreadName을 지정할 수 있다. 디버깅할 때 좋다.
AsyncTask의 doInBackground에서는 이 task가 어디서 call되었는지 파악이 되지 않는다. ExAsyncTask에서는 execute한 caller 쓰레드의 call stack 최근 5개를 제공한다. 역시 디버깅할 때 좋다.
- showProgressDialog를 task에서 제공한다.
시간이 걸리는 task에 progress dialog를 띄우는 것은 자주 사용하는 패턴이지만 매번 구현하기 귀찮다. 기본적으로 task가 완료될 때까지 cancel할 수 없지만, timeout 시간(초 단위)가 지나면 cancel 버튼이 활성화되고, task를 취소할 수 있다. 무한히 기다리게 만드는 앱은 앱을 강제종료할 수 밖에 없기에 문제가 있다.
- doInBackground에서 Exception를 catch하지 않아도 앱이 크래시되지 않는다.
OutOfMemory error가 발생하게 되면 예외적으로 System.gc()를 수행해주고 crash를 막는다. 그 외 Error는 crash된다.
public abstract class ExAsyncTask<Params, Progress, Result> implements Canceller { private static final String TAG = "ExAsyncTask"; protected Exception exception;
private static final ExecutorService defaultExecutorServier = CommonExecutors.getCachedThreadPool(); private static final InternalHandler handler = new InternalHandler(); private static final int MESSAGE_POST_RESULT = 1; private static final int MESSAGE_POST_PROGRESS = 2; private static final int MESSAGE_POST_SHOW_PROGRESS_DIALOG = 3;
public final ExAsyncTask<Params, Progress, Result> execute(Params... parameters) { executeOnExecutor(defaultExecutorServier, parameters); return this; }
public final ExAsyncTask<Params, Progress, Result> executeOnExecutor(ExecutorService executorService, final Params... parameters) { // execute call stack for debugging if (BuildConfig.DEBUG) { int lineCount = 0; StringBuilder sb = StringBuilderPool.obtain(); for (StackTraceElement e : Thread.currentThread().getStackTrace()) { if (e.getClassName().equals("dalvik.system.VMStack") == false && e.getClassName().equals("java.lang.Thread") == false && e.getClassName().equals(ExAsyncTask.class.getName()) == false) { sb.append(e.toString()); sb.append("\n"); lineCount++; if (lineCount > 5) { break; } } } callStack = sb.toString(); StringBuilderPool.recycle(sb); }
cancelled.set(false); exception = null; taskState = AsyncTask.Status.PENDING; future = null; onPreExecute(); future = executorService.submit(new Callable<Result>() { @Override public final Result call() throws Exception { Result result = null; taskState = AsyncTask.Status.RUNNING; try { if (BuildConfig.DEBUG) { Log.d("ExAsyncTask", String.format("ExAsyncTask [%s] called", threadName != null ? threadName : ExAsyncTask.this.getClass().getName())); Thread.currentThread().setName(String.format("ExAsyncTask [%s]", threadName != null ? threadName : ExAsyncTask.this.getClass().getName())); } result = doInBackground(parameters); return result; } catch (final Throwable ex) { if (ex instanceof OutOfMemoryError) { System.runFinalization(); System.gc(); exception = new RuntimeException(ex); } else if (ex instanceof Error) { throw ex; // crash } else if (ex instanceof Exception) { exception = (Exception)ex; } else { exception = new RuntimeException(ex); } return null; } finally { if (BuildConfig.DEBUG) { Thread.currentThread().setName(""); } Message message = handler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(ExAsyncTask.this, result)); message.sendToTarget(); } } }); return this; }
@Override public final boolean cancel(boolean mayInterruptIfRunning) { cancelled.set(true); return future != null && future.cancel(mayInterruptIfRunning); }
@Override public final boolean isCancelled() { return cancelled.get(); }
public final Result get() throws Exception { if (future == null) { throw new RuntimeException("future is null. call wait after execute"); }
return future.get(); }
public final Result get(long timeout, TimeUnit timeUnit) throws Exception { if (future == null) { throw new RuntimeException("future is null. call wait after execute"); }
public final AsyncTask.Status getStatus() { return taskState; }
protected void onPreExecute() { }
protected abstract Result doInBackground(Params... params);
protected final void publishProgress(Progress... values) { if (!isCancelled()) { handler.obtainMessage(MESSAGE_POST_PROGRESS, new AsyncTaskResult<Progress>(this, values)).sendToTarget(); } }
public final ExAsyncTask setThreadName(String threadName) { this.threadName = threadName; return this; }
public ExAsyncTask showProgressDialog(Activity activity) { return showProgressDialog(activity, 0); }
public ExAsyncTask showProgressDialog(Activity activity, int timeoutSecond) { if (this.activityHelper == null) { if (activity instanceof BaseActivity) { this.activityHelper = ((BaseActivity)activity).helper; } else { this.activityHelper = new ActivityHelper(activity); } } handler.obtainMessage(MESSAGE_POST_SHOW_PROGRESS_DIALOG, new AsyncTaskResult<Integer>(this, new Integer[]{timeoutSecond})).sendToTarget(); return this; }
private static class InternalHandler extends android.os.Handler { @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult)msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: // There is only one result result.task.finish(result.data[0]); break; case MESSAGE_POST_PROGRESS: result.task.onProgressUpdate(result.data); break; case MESSAGE_POST_SHOW_PROGRESS_DIALOG: result.task.activityHelper.showProgressDialog(true, result.task, (Integer)result.data[0]); break; } } }
private static class AsyncTaskResult<Data> { ExAsyncTask task; Data[] data;