2014-12-06

AsyncTask 與 UI Thread

緣由

在開發 Google Cloud Message(GCM)時遇到 AsyncTask,但沒有提供 sendRegistrationIdToBackend() 實做,事實上沒辦法也不需要,這是個人的事。

一開始我是用 HttpClient 將 registrationId 送到後台 Web Server,這樣沒問題,可是後來 Web Server 改用 Http over SSL,也就是 https,哇靠,HttpClient 預設不通未公開憑證的 https 網站。

後來使了個小聰明,當時 Activity 裡有個 WebView 元件,那就用 WebView 元件來送 request,應該就沒問題吧!

果然,只要 WebView 加上以下的設定,就可以通 https 了。
this.webview.setWebViewClient(new WebViewClient() {
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        Log.e(TAG, "onReceivedSslError - " + error);
        handler.proceed();
    }
});
順利交差沒多久,暴了。

說在新版本的 Android 上 App 會閃退,我的測試版本是 Android 2.3,在 Android 4.4 上會閃退,這..........原因很難找。

最後終於搞清楚,是 UI Thread 在作怪。

由於 sendRegistrationIdToBackend() 是費時的工作(要連後台的 Server),所以 GCM 用 AsyncTask 的 doInBackground() 來執行 sendRegistrationIdToBackend(),一開始我在 doInBackground() 用 HttpClient 來傳資料沒問題,但是後來改用 WebView 就不行了,因為 WebView 是 UI 物件,UI 物件只能在 UI Thread 裡操作,而 AsyncTask 的 doInBackground() 當然是 Background Thread 啊,不然用它做啥!

UI Thread

根據 Communicating with the UI Thread 裡說的:

Every app has its own special thread that runs UI objects such as View objects; this thread is called the UI thread. Only objects running on the UI thread have access to other objects on that thread. Because tasks that you run on a thread from a thread pool aren't running on your UI thread, they don't have access to UI objects. To move data from a background thread to the UI thread, use a Handler that's running on the UI thread.

擷取重點如下:
  1. 每個 App 都會有一個(僅此一個)負責管理所有 UI 物件的 Thread,就叫做 UI Thread,要操作任何 UI 物件都得透過這個 Ui Thread。
  2. 任何 Background Thread,像是 Thread Pool 來的,或是 AsyncTask,都不能呼叫 UI 物件的任何 API,否則,App 立刻死給你看,沒有 Exception 喔(catch 不到),App 直接打卡下班。
  3. Background Thread 要跟 UI 互動的話,Thread Pool 來的可以透過 Handler,AsyncTask(一種簡易版的 Background Thread)可以呼叫特定的 API。

AsyncTask

主要有四加一個 method。

第一個是 onPreExecute()由 UI Thread 呼叫,這相當重要,表示可以在這呼叫 UI 物件,一般就是用來顯示進度列,表示背景程式的執行進度。

第二個是 doInBackground(Params...),接在 onPreExecute() 後面執行,由 Background Thread 呼叫,用來執行費時的工作,由於這是在 Background Thread 裡執行,因此不能在這裡面呼叫 UI 物件。

此時可以說是和休士頓失聯的狀態嗎?

不,可以呼叫 publishProgress(Progress...) 來更新 UI 物件,疑?不是說不能動 UI 嗎?

嘿嘿,AsyncTask 留了一手,publishProgress(Progress...) 是 AsyncTask 的 API,但它不會直接動 UI,只是留個記號,另外會有人負責來檢查這個記號,一旦發現有新資料,就會呼叫 AsyncTask 第三個 method,就是 onProgressUpdate(Progress...)。

第三個是 onProgressUpdate(Progress...)由 UI Thread 呼叫,由 publishProgress(Progress...) 觸發,執行時間是不確定的,這個 method 一般就是用來更新進度列的(或者說更新 UI 的),doInBackground(Params...) 在呼叫 publishProgress(Progress...) 時,會將目前進度傳進去,當然可以傳其他東西,而 onProgressUpdate(Progress...) 被呼叫時,就會得到目前進度(或其他東西)。

切記,要傳給 onProgressUpdate(Progress...) 的資料一定要透過 publishProgress(Progress...) 的參數,不可以透過 AsyncTask 的 class 變數,因為剛說過,onProgressUpdate(Progress...) 的執行時間點是不確定的,如果第一個 onProgressUpdate(Progress...) 尚未執行,doInBackground(Params...) 又呼叫了一次 publishProgress(Progress...),那前一個尚未更新的 class 變數就會被覆蓋了。

最後一個是 onPostExecute(Result),也就是在 doInBackground(Params...) 結束之後執行,不意外的,也是由 UI Thread 呼叫,doInBackground(Params...) 回傳的物件會傳給 onPostExecute(Result),不過這時倒是可以用 class 變數來傳啦,因為只會執行一次。

整理如下:
  • 只有在 doInBackground(Params...) 裡不能動 UI
  • 在 doInBackground(Params...) 若想動 UI,得間接透過 publishProgress(Progress...) 與 onProgressUpdate(Progress...),或者等到 onPostExecute(Result) 再更新 UI。

最後回到我的問題,就是我在 doInBackground(Params...) 裡呼叫 WebView 來傳資料,WebView 是 UI 的人,不行!

最惱人的是,是 App 直接閃退,完全沒訊息,只能 Eclipse 接 Device,才能在 Console 裡看到死因。
---
---
---

沒有留言:

張貼留言