2011-12-19

在 Android 裡使用 AsyncTask 來處理費時的運算

由於 Android 有五秒的 ANR 限制,所以像是網路存取(HttpGet 或 HttpPost)這類費時的運算,就不適合由 Main Thread 處理。

因為 Android UI 不是 Thread safe,因此所有的 UI 操作都得透過 Main Thread,而當費時的運算過程或者結果不會影響到 UI 的話,那只要起一個 Thread 去處理就可以了,但是當過程或結果需要與 UI 互動時,就得用上 AsyncTask。

AsyncTask 會起一個背景執行的 Thread,並提供前中後三個 callback 供開發者使用。

至於 AsyncTask 與同樣可以更新 UI 的 Handler + Worker Thread 的差異,改天再做研究。

main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="開始計算" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
Calculator
public class Calculator extends AsyncTask<List<Integer>, Integer, Integer> {

    private static final String TAG = "Calculator";
    private Context ctx;
    private TextView tv;
    private Integer result;
    private Integer progress;

    public Calculator(Context ctx) {
        this.setContext(ctx);
    }

    public void setContext(Context ctx) {
        // 用來更新 UI
        this.ctx = ctx;
        this.tv = (TextView) ((Activity) this.ctx).findViewById(R.id.tv);
        this.progressUI();
    }

    @Override
    protected Integer doInBackground(List<Integer>... params) {
        LogUtils.thread("執行計算...");
        // 在 Worker Thread 裡執行,所以不可更新 UI
        this.result = 0;
        Integer p;
        for (int i = 0; i < params[0].size(); i++) {
            p = params[0].get(i);
            this.result += p.intValue();
            LogUtils.thread("計算過程..." + this.result);
            // 更新進度
            this.publishProgress(100 * (i + 1) / params[0].size());
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
            }
        }
        LogUtils.thread("執行計算完成..." + this.result);
        return this.result;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        LogUtils.thread("計算前...");
        // 在 Main Thread 裡執行,可更新 UI
        this.progress = 0;
    }

    @Override
    protected void onPostExecute(Integer result) {
        super.onPostExecute(result);
        this.result = result;
        LogUtils.thread("計算結果..." + this.result);
        // 在 Main Thread 裡執行,可更新 UI
        this.syncUI();
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        this.progress = values[0];
        LogUtils.thread("更新進度..." + this.progress);
        // 在 Main Thread 裡執行,可更新 UI
        this.progressUI();
    }

    public void syncUI() {
        LogUtils.thread("更新 UI...");
        this.tv.setText("計算結果..." + this.result);
    }

    private void progressUI() {
        LogUtils.thread("更新進度...");
        this.tv.setText("目前進度..." + this.progress);
    }
}
AsyncTaskActivity
public class AsyncTaskActivity extends Activity {

    private static final String TAG = "AsyncTaskActivity";
    private Calculator task;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        // 重新連線
        this.task = (Calculator) this.getLastNonConfigurationInstance();
        if (this.task != null) {
            Log.d(TAG, "與 AsyncTask 重新連線...");
            this.task.setContext(this);
            // 在 Activity 重起過程中已計算完畢,所以要呼叫 AsyncTask 來更新 UI
            if (this.task.getStatus() == AsyncTask.Status.FINISHED) {
                this.task.syncUI();
            }
        }
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        // 保留 Activity 與 AsyncTask 的關聯
        // 供 Activity 因故重起後,可以找回已在執行的 AsyncTask
        return this.task;
    }

    public void onClick(View v) {
        if (this.task != null) {
            AsyncTask.Status status = this.task.getStatus();
            Log.d(TAG, "目前狀態:" + status);
            if (status != AsyncTask.Status.FINISHED) {
                Log.d(TAG, "前一計算未完成,不可重起計算");
                return;
            }
            Log.d(TAG, "前一計算已完成,可重起計算");
        }
        Log.d(TAG, "新計算...");
        this.task = new Calculator(this);
        List<Integer> params = new ArrayList<Integer>();
        for (int i = 0; i < 10; i++) {
            params.add(i);
        }
        // 只能呼叫一次 execute(...)
        this.task.execute(params);
        // 可以手動中斷
        // this.task.cancel(true);
        // 判斷是否終止
        // if (this.task.isCancelled()){ }
        // 也可以取得計算結果
        // Integer result = this.task.get();
    }
}
LogUtils
public class LogUtils {

    private static final String TAG = "LogUtils";

    public static final void thread() {
        LogUtils.thread(null);
    }

    public static final void thread(String msg) {
        Thread t = Thread.currentThread();
        Log.d(TAG,
                "<" + t.getName() + ">id: " + t.getId() + ", Priority: "
                        + t.getPriority() + ", Group: "
                        + t.getThreadGroup().getName()
                        + (msg != null ? ", Msg: " + msg : ""));
    }
}
相關文章

2 則留言:

  1. 原來Android有內建這個東西,我之前都用Thread自己去寫、去調...好像還有一個ActitivyThread的API,我改天也來研究看看

    回覆刪除
  2. 原來還有這個API可以使用啊,我之前都是用Thread自己寫、自己修runtime error...

    回覆刪除