Android開發者:你真的會用AsyncTask嗎?

OneAPM官方技術部落格發表於2015-06-04

導讀】在Android應用開發的過程中,我們需要時刻注意保證應用程式的穩定和UI操作響應及時,因為不穩定或響應緩慢的應用將給應用帶來不好的印象,嚴重的使用者解除安裝你的APP,這樣你的努力就沒有體現的價值了。本文試圖從AsnycTask的作用說起,進一步的講解一下內部的實現機制。如果有一些開發經驗的人,讀完之後應該對使用AsnycTask過程中的一些問題豁然開朗,開發經驗不豐富的也可以從中找到使用過程中的注意點。

為何引入AsnyncTask?

在Android程式開始執行的時候會單獨啟動一個程式,預設情況下所有這個程式操作都在這個程式中進行。一個Android程式預設情況下只有一個程式,但是一個程式卻是可以有許執行緒的。

在這些執行緒中,有一個執行緒叫做UI執行緒,也叫做Main Thread,除了Main Thread之外的執行緒都可稱為Worker Thread。Main Thread主要負責控制UI頁面的顯示、更新、互動等。因此所有在UI執行緒中的操作要求越短越好,只有這樣使用者才會覺得操作比較流暢。一個比較好的做法是把一些比較耗時的操作,例如網路請求、資料庫操作、複雜計算等邏輯都封裝到單獨的執行緒,這樣就可以避免阻塞主執行緒。為此,有人寫了如下的程式碼:

private TextView textView;
    public void onCreate(Bundle bundle){
        super.onCreate(bundle);
        setContentView(R.layout.thread_on_ui);
        textView = (TextView) findViewById(R.id.tvTest);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpGet httpGet = new HttpGet("http://www.baidu.com");
                    HttpClient httpClient = new DefaultHttpClient();
                    HttpResponse httpResp = httpClient.execute(httpGet);
                    if (httpResp.getStatusLine().getStatusCode() == 200) {
                        String result = EntityUtils.toString(httpResp.getEntity(), "UTF-8");
                        textView.setText("請求返回正常,結果是:" + result);
                    } else {
                    textView.setText("請求返回異常!");
                }
            }catch (IOException e){
               e.printStackTrace();
            }
        }
    }).start();
}

執行,不出所料,異常資訊如下:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

怎麼破?可以在主執行緒建立Handler物件,把textView.setText地方替換為用handler把返回值發回到handler所在的執行緒處理,也就是主執行緒。這個處理方法稍顯複雜,Android為我們考慮到了這個情況,給我們提供了一個輕量級的非同步類可以直接繼承AsyncTask,在類中實現非同步操作,並提供介面反饋當前非同步執行的結果以及執行進度,這些介面中有直接執行在主執行緒中的,例如onPostExecute,onPreExecute等方法。

也就是說,Android的程式執行時是多執行緒的,為了更方便的處理子執行緒和UI執行緒的互動,引入了AsyncTask。

AsnyncTask內部機制

AsyncTask內部邏輯主要有二個部分:

1、與主線的互動,它內部例項化了一個靜態的自定義類InternalHandler,這個類是繼承自Handler的,在這個自定義類中繫結了一個叫做AsyncTaskResult的物件,每次子執行緒需要通知主執行緒,就呼叫sendToTarget傳送訊息給handler。然後在handler的handleMessage中AsyncTaskResult根據訊息的型別不同(例如MESSAGE_POST_PROGRESS會更新進度條,MESSAGE_POST_CANCEL取消任務)而做不同的操作,值得一提的是,這些操作都是在UI執行緒進行的,意味著,從子執行緒一旦需要和UI執行緒互動,內部自動呼叫了handler物件把訊息放在了主執行緒了。原始碼地址

mFuture = new FutureTask<Result>(mWorker) {
       @Override
        protected void More ...done() {
            Message message;
           Result result = null;

            try {
                result = get();
           } catch (InterruptedException e) {
                android.util.Log.w(LOG_TAG, e);
           } catch (ExecutionException e) {
                throw new RuntimeException("An error occured while executing doInBackground()",
                        e.getCause());
            } catch (CancellationException e) {
                message = sHandler.obtainMessage(MESSAGE_POST_CANCEL,
                       new AsyncTaskResult<Result>(AsyncTask.this, (Result[]) null));
                message.sendToTarget();
                return;
            } catch (Throwable t) {
                throw new RuntimeException("An error occured while executing "
                       + "doInBackground()", t);
            }

            message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                    new AsyncTaskResult<Result>(AsyncTask.this, result));
            message.sendToTarget();
       }
    };


private static class InternalHandler extends Handler {
    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void More ...handleMessage(Message msg) {
        AsyncTaskResult result = (AsyncTaskResult) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
            case MESSAGE_POST_CANCEL:
                result.mTask.onCancelled();
                break;
        }
    }
}

2、AsyncTask內部排程,雖然可以新建多個AsyncTask的子類的例項,但是AsyncTask的內部Handler和ThreadPoolExecutor都是static的,這麼定義的變數屬於類的,是程式範圍內共享的,所以AsyncTask控制著程式範圍內所有的子類例項,而且該類的所有例項都共用一個執行緒池和Handler。程式碼如下:

public abstract class AsyncTask<Params, Progress, Result> {
private static final String LOG_TAG = "AsyncTask";

private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;

private static final BlockingQueue<Runnable> sWorkQueue =
        new LinkedBlockingQueue<Runnable>(10);

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread More ...newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
        MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);

private static final int MESSAGE_POST_RESULT = 0x1;
private static final int MESSAGE_POST_PROGRESS = 0x2;
private static final int MESSAGE_POST_CANCEL = 0x3;

從程式碼還可以看出,預設核心執行緒池的大小是5,快取任務佇列是10。意味著,如果執行緒池的執行緒數量小於5,這個時候新新增一個非同步任務則會新建一個執行緒;如果執行緒池的數量大於等於5,這個時候新建一個非同步任務這個任務會被放入快取佇列中等待執行。限制一個APP內AsyncTask併發的執行緒的數量看似是有必要的,但也帶來了一個問題,假如有人就是需要同時執行10個而不是5個,或者不對執行緒的多少做限制,例如有些APP的瀑布流頁面中的N多圖片的載入。

另一方面,同時執行的任務多,執行緒也就多,如果這些任務是去訪問網路的,會導致短時間內手機那可憐的頻寬被佔完了,這樣總體的表現是誰都很難很快載入完全,因為他們是競爭關係。所以,把選擇權交給開發者吧。

事實上,大概從Android從3.0開始,每次新建非同步任務的時候AsnycTask內部預設規則是按提交的先後順序每次只執行一個非同步任務。當然了你也可以自己指定自己的執行緒池。

可以看出,AsyncTask使用過程中需要注意的地方不少

  • 由於Handler需要和主執行緒互動,而Handler又是內建於AsnycTask中的,所以,AsyncTask的建立必須在主執行緒。
  • AsyncTaskResult的doInBackground(mParams)方法執行非同步任務執行在子執行緒中,其他方法執行在主執行緒中,可以操作UI元件。
  • 不要手動的去呼叫AsyncTask的onPreExecute, doInBackground, publishProgress, onProgressUpdate, onPostExecute方法,這些都是由Android系統自動呼叫的
  • 一個任務AsyncTask任務只能被執行一次。
  • 執行中可以隨時呼叫cancel(boolean)方法取消任務,如果成功呼叫isCancelled()會返回true,並且不會執行onPostExecute() 方法了,取而代之的是呼叫 onCancelled() 方法。而且從原始碼看,如果這個任務已經執行了這個時候呼叫cancel是不會真正的把task結束,而是繼續執行,只不過改變的是執行之後的回撥方法是onPostExecute還是onCancelled。

AsnyncTask和Activity OnConfiguration

上面提到了那麼多的注意點,還有其他需要注意的嗎?當然有!我們開發App過程中使用AsyncTask請求網路資料的時候,一般都是習慣在onPreExecute顯示進度條,在資料請求完成之後的onPostExecute關閉進度條。這樣做看似完美,但是如果您的App沒有明確指定螢幕方向和configChanges時,當使用者旋轉螢幕的時候Activity就會重新啟動,而這個時候您的非同步載入資料的執行緒可能正在請求網路。當一個新的Activity被重新建立之後,可能由重新啟動了一個新的任務去請求網路,這樣之前的一個非同步任務不經意間就洩露了,假設你還在onPostExecute寫了一些其他邏輯,這個時候就會發生意想不到異常。

一般簡單的資料型別的,對付configChanges我們很好處理,我們直接可以通過onSaveInstanceState()和onRestoreInstanceState()進行儲存與恢復。Android會在銷燬你的Activity之前呼叫onSaveInstanceState()方法,於是,你可以在此方法中儲存關於應用狀態的資料。然後你可以在onCreate()或onRestoreInstanceState()方法中恢復。

但是,對於AsyncTask怎麼辦?問題產生的根源在於Activity銷燬重新建立的過程中AsyncTask和之前的Activity失聯,最終導致一些問題。那麼解決問題的思路也可以朝著這個方向發展。Android官方文件也有一些解決問題的線索。

這裡介紹另外一種使用事件匯流排的解決方案,是國外一個安卓大牛寫的。中間用到了Square開源的EventBus類庫http://square.github.io/otto/。首先自定義一個AsyncTask的子類,在onPostExecute方法中,把返回結果拋給事件匯流排,程式碼如下:

 @Override 
protected String doInBackground(Void... params) {
    Random random = new Random();
    final long sleep = random.nextInt(10);
    try {
        Thread.sleep(10 * 6000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "Slept for " + sleep + " seconds";
}

@Override
protected void onPostExecute(String result) {
    MyBus.getInstance().post(new AsyncTaskResultEvent(result));
}

在Activity的onCreate中註冊這個事件匯流排,這樣非同步執行緒的訊息就會被otta分發到當前註冊的activity,這個時候返回結果就在當前activity的onAsyncTaskResult中了,程式碼如下:

 @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.otto_layout);

    findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            new MyAsyncTask().execute();
        }
    });

    MyBus.getInstance().register(this);
}

@Override
protected void onDestroy() {
    MyBus.getInstance().unregister(this);
    super.onDestroy();
}

@Subscribe
public void onAsyncTaskResult(AsyncTaskResultEvent event) {
    Toast.makeText(this, event.getResult(), Toast.LENGTH_LONG).show();
}

個人覺的這個方法相當好,當然更簡單的你也可以不用otta這個庫,自己單獨的用介面回撥的方式估計也能實現,大家可以試試。

如何監控AsyncTask?

不過,AsyncTask雖然很好用,但是問題也不少,需要注意的地方也很多。萬一又出現問題了怎麼辦?客戶反饋一個問題,開發人員第一反應是:“這不可能啊,我這裡測試沒問題的!”但是,老闆讓你解決問題,客戶的環境無法復現、操作步驟無法重複,這個時候開發就犯難了......總之,監控這個事情是非常重要的。

以應用效能管理(APM)領軍企業OneAPM為例,其提供了新一代應用效能管理軟體和服務,能夠幫助企業使用者和開發者輕鬆實現,緩慢的程式程式碼和SQL語句的實時抓取。OneAPM推出了針對移動端的應用效能監控產品Mobile Insight,使用者可以訪問OneAPM官方網站,下載移動端監控SDK,測試下AsyncTask。


本文作者系OneAPM工程師編譯整理。OneAPM是中國基礎軟體領域的新興領軍企業。專注於提供下一代應用效能管理軟體和服務,幫助企業使用者和開發者輕鬆實現:緩慢的程式程式碼和SQL語句的實時抓取。想閱讀更多技術文章,請訪問OneAPM官方技術部落格

相關文章