優化 Android 線程和后臺任務開發
來自: https://realm.io/cn/news/android-threading-background-tasks/
Sign up to be notified of new videos — we won’t email you for any other reason, ever.
Android 線程 (0:46)
當我們談論線程,我們知道一個 Android 應用程序至少有一個主線程。這個線程是隨著你的 Application 類的創建而同時創建的。主線程的關鍵職責就是繪制用戶界面:它是為了處理用戶交互,在屏幕上繪制像素,并進行加載 Activity。任何你添加到 Activity 里和加載資源有關的代碼,都將被放置于主線程處理結束才運行,因此應用程序才能保持對于用戶操作的即時響應。
你可能認為這是沒有什么大不了的,例如,您可以有一個列表視圖(ListView),建立一個列表,并添加一些項目。你可以將列表數據排序,然后把它傳給一個列表適配器。有了資源,處理器將嘗試為您繪制列表。
然而,你添加到 Activity 中的一些代碼可能需要比較長時間的計算,它們不應該是在主線程中進行運行的。一個比較常見的例子是:在你的應用程序中嘗試做一個網絡調用。你可以試著去獲得一個 URLConnection 的默認實例,并在 Activity 中運行它。這在編譯的時候是不會出什么問題,但在運行時,你將會得到一個 NetworkOnMainThreadException 。
讓我們再回到一個不太極端的例子,回到列表視圖。對于一個非靜態列表,你想查詢本地數據庫,并將結果插入你的列表中。如果你使用Realm,那么在 UI 線程(主線程)做這個查詢是沒有問題的。但是,讓我們假設你使用的是 SQLite 數據庫。做一個查詢請求需要大量的代碼,訪問數據庫引擎,得到的結果,對它們進行排序,并把它們交給你的 ArrayAdapter 。如果所有的這一切都運行在 Activity ,你的用戶界面交互可能就很不順暢了。
以前你可能會覺得,在 UI 線程讀取 shared preferences,并沒有什么不好的事情發生。它能有什么影響?隨著你越來越多的任務,去中斷或搶占 UI 線程的時間,應用程序將更多地傾向于跳過或滯后處理一些 UI 更新,經常就是導致動畫將無法正確更新。用戶可能會困惑這個應用是怎么了,什么可能發生最壞的情況就是,如果用戶等待了一定長時間,他們會得到的系統發出的錯誤消息說,“應用程序沒有響應。你想繼續等待還是殺死這個應用?”這就相當于你走在路上,突然被貼上了一個“干掉我”的標簽。就算用戶留下來了他們也可能不知道當前應用程序是否是值得信賴的了。
“每個線程都會分配一個私有內存區域,該區域主要用于在執行過程中存儲方法、局部變量和參數。一旦線程終止私有內存區將會被銷毀。” - Anders G?ransson, 《高效 Android 線程開發(Efficient Android Threading)》 一書作者
我真的很喜歡這句話,它來自 Efficient Android Threading 這本書,這本書講了什么是線程和線程做什么事情。它可以幫助我了解什么是后臺線程,以及它們是如何在主線程中運行的。這句話也是提醒你注意一個事實,你是你創造的線程的調度者,你應該對他們負責。Android 有很多選項或方式來協助你進行線程調度。但你應該牢記 如果你不是在主線程進行一些操作,那么你要知道你的哪些線程是目前正在運行的。
最后一點我想提出的是,對于多線程,每次執行跳躍從一個線程到另一個,會有一個短暫時間的延遲。這就是所謂的上下文切換。接下來我們將更多地討論類似這樣的鏈式操作,和優化一些復雜的任務系列。
AsyncTask (5:51)
現在,我想談談幾個 Android SDK 提供的管理線程的工具。首先就是 AsyncTask ,它能夠使你很方便地使用它在 Activity 中執行異步任務。但這僅僅是你 能夠 ,而不是你“應該”。
在 AsyncTask 中你首先會看到的兩個方法之一就是 doInBackground ,這是你可以在其中做耗時任務的后臺線程調用的方法。第二種方法是 onPostExecute ,它將運行于主線程。你可以 doInBackground 發送信息給 onPostExecute ,然后繼續執行后臺任務。
那么 AsyncTask 的原理是什么呢?你可以 按住ctrl + 鼠標單擊 這個類的名字進入到它的源代碼文件。我覺得真的值得去閱讀 AsyncTask 的代碼,因為它會告訴你很多關于 Android 的線程模型是什么樣的、它們是怎么運行的。
// AsyncTask.java
private static class InternalHandler extends Handler {
@Override
public void 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;
}
}
}
這個類中最有趣的部分是有一個 handler 實例。handler 的主要工作是提供一種替代方法,以編寫一系列方法,并希望它們能夠互相調用,完成一系列的任務。當你向他發生事件的時候,在它的 handleMessage 中可以使用 switch 來執行不同的響應代碼。
AsyncTask 通過 execute 來啟動并執行一個線程。然后,你寫在 doInBackground 中的代碼就會自動運行。你無需關心的一點是,handler 會發送一個 MESSAGE_POST_RESULT 信號。這個信號使得 onPostExecute 被調用。這聽起來很方便,但要記住在使用它的時候,不要隨著時間的推移使簡單的任務變得越來越復雜,那時 AsyncTask 可能就會變得很臃腫和難以控制。
讓我們假設我所描述的情況實際上并不足以滿足你的需要。那么可以看看接下來這個例子,你有兩個任務,需要按順序運行,這兩個都是異步實現的。因為你已經使用 AsyncTask 運行一個后臺線程,你可能會開始的第一個任務,然后在 doInBackground 中啟動第二任務。這是 Android 線程模型不允許的事情。因此,可能的做法就是等待第一個任務完全結束再于主線程中啟動第二個任務。這在 JavaScript 中也經常會類似這么回調。不過,雖然你可以這么做,但代碼上看起來就經常會很糟糕:
new AsyncTask<Void, Void, Boolean>() {
protected Boolean doInBackground(Void... params) {
doOneThing();
return null;
}
protected void onPostExecute(Boolean result) {
AsyncTask doAnotherThing =
new AsyncTask<Void, Void, Boolean>() {
protected Boolean doInBackground(Void... params) {
doYetAnotherThing();
return null;
}
private void doYetAnotherThing() {}
};
doAnotherThing.execute();
}
private void doOneThing() {}
}.execute();
如果你仍然感到困惑,覺得這個代碼并不會太難以閱讀。但我要告訴你這個代碼是很 糟糕 的,它顯得混亂和嵌套嚴重。原因是因為在這里,你開啟了一個新的線程,請求 AsyncTask 的整個生命周期去執行你的第一個任務。你調用 doOneThing ,然后返回到UI線程。就在那一刻,你又一次的阻塞了用戶界面線程,因為沒有其他的辦法去開始另一個后臺任務線程。這點很重要。你是交織地使用 UI 線程去管理你的線程,但實際上沒有一個很好的理由這樣做。正如我之前提到的,你每次這么做的時候都要引起一個計算延遲或稱主線程卡頓。而且,事實上,這是一個不可維護的代碼塊,基于設計和架構的理由我們不應該這么做。
在這種情況下,我們有一個復雜的任務,有一些其他的事情。Android SDK 提供了許多的選項,因此,對于大多數情況,你不必處理啟動和管理自己的線程。
IntentService (11:57)
如果你認為我是在為你使用 Services 的話題升溫的話,你是對的。我用了一個 IntentService 實施以下用例。通過 IntentService ,可以順接執行另一個線程。它可以運行在完全獨立而不需要 UI 線程做新線程或新任務的交織,它甚至可以在應用程序不在前臺的時候進行運行。使用 IntentService 的特別好處是你不必自己去啟動和關閉線程。
所有 services 都是從 activity 開始的。它們負責更重的責任,并且初始化也比 AsyncTask 更難一些,但它們也只是需要更多的設置和在 Manifest 文件中聲明一下而已。你可以通過 Intent Bundle 來傳遞你的意圖。我們會用一個例子來展示如何使用 Activity 和 后臺任務 進行交互。
Activity 和 Service 的通訊(13:29)
提供一個關于這個話題的最佳實踐: CodePath guides 。現在, Activity 啟動了一個 Service 。 Activity 還感興趣的是能夠與 Service 溝通,這樣我們就可以知道什么時候服務結束了。我們可以創建 ResultReceiver 對象來給出反饋。通過 Intent 將這個 receiver 傳遞到 Service 。這樣以后, Service 有一個 ResultReceiver 對象,將運行用戶界面代碼。 Service 去做它應該做的。在最后的時刻,我們在 onHandleIntent 結束之前調用 rr.send 。我們可以發送一個 RESULT 標志和一個數據包。所以,當你調用 rr.send ,你的 Activity 就能接收到它了。
// Activity
ResultReceiver rr = new
ResultReceiver() {
@Override
onReceiveResult() {
// do stuff!
}
}
// add rr to extras
startService(myServiceIntent)
// Service
onHandleIntent(Intent i) {
ResultReceiver rr =
i.getExtras().get(RR_KEY)
(assigned from bundle)
rr.send(Activity.RESULT_OK
successData);
}
設計任務通信(14:57)
假設現在,我們已經建立起了越來越多的復雜任務。我想分享一個我在“芒果健康”所做的工作內容,當時我在寫應用程序登錄部分的解決方案。對于我們的應用程序,它是可以無需登錄而使用的。然后,我們向用戶提供登錄的功能,以便他們能夠把本地數據同步到遠程服務器。如果我們想支持與遠程服務器同步數據的話,登錄過程的設計就變得比較重。所以,為了幫助我設計登錄過程,我給它定了五個原則:
- 最關鍵的就是讓整個任務在后臺線程上運行。我們不想讓同步工作在 UI 線程中進行。UI的責任是當他們登錄,顯示加載旋轉和通知用戶。
- 任務應該是可以發出信號,通知是否登錄成功或失敗。
- 我們知道這比較復雜,因為這里有多個任務并且我們得按照一定的順序來執行它們。
- 一些任務將是異步的,比如數據庫同步。但這一步應該不是獨立于其他的事情,一個任務接著一個任務。
- 最后,我們寫的代碼應該能夠讓人感到易讀。
創建一個登錄任務(16:45)
建立一個登錄任務需要很多步驟,經常令人比較頭疼的事要弄清楚要有哪些任務需要運行并且他們順序是怎么樣的。可能有些要同步進行有些要異步進行。我特別列出了以下步驟,這些過程是異步的:
- 獲得網絡請求的身份驗證令牌(auth token)
- 通過網絡請求獲取用戶賬戶
- 獲取遠程數據庫與新的本地數據庫
- 整合新數據庫
這四個任務需要在流程中進行擬合。我們也有一些其他的任務,可以同步運行。其中設計的第一原則就是不能阻塞的用戶界面線程。所以,對任務進行分解能夠讓我們更好地去開發好的應用。我還考慮了對于某些任務,要如何給 Activity 進行反饋。在一個過程中任何一個任務失敗,用戶都將無法繼續啟動。最后,我們完成了所有的步驟,并且可以顯示登錄頁面了。
整個任務的清單可以歸結為兩個主要的原則。首先,試著找出 可以把潛在的復雜的任務分解成更小的任務 的方法。第二,嘗試在這些方法中向 Activity 發送失敗或者成功的反饋。
建立登錄狀態(19:03)
這就是我所說的回調鏈的例子,如果你可以同時運行兩套操作,是因為他們彼此相對獨立。但是在一個有多個異步任務的情況下,并且這些異步任務有聯系和先后順序,事情就變得不一樣了。比如在我們的情況下,你必須得從服務器上獲得一個登錄 token,才能拿著這個 token 進行后續操作。如果失敗了,那么我們得立即退出。但是,如果它成功,那么我們可以繼續進行下一個異步任務。因此,你最終會擁有這一系列的工作,所有的工作都需要以異步運行。
讓我們來看一看,我認為可以解決這個問題的一個方法。我試著將這整個事情看作為一個可以從 Service 開始的東西,但會以一個模塊的形式運行。我有一個類,有許多方法,當我們滾動代碼頁面,我們只是看到更多的方法,這些方法各自做的任務不一樣。這是一個粗糙的方法,這個類的代碼會比較多。我寫這個類的存在的問題是,它真的,真的很難記住怎么才能讓所有的零碎代碼一起工作。我什么時候調用某個方法?為什么我要調用這個方法?
如果我是從以前的開發人員手里接手到這份代碼,我就很難知道這個類應該做什么。這里有一個登錄管理器,但我不知道什么時候登錄是真正成功的。原因是,在回調這些異步過程,還有另一個方法調用其他方法。
消息處理程序(21:58)
一定程度上來說,以下這個代碼是有組織的。它運作得不錯,但并不是一個偉大的解決方案。我想談一談我們能回到一個更好的解決方案。幸運的是,我們不需要寫一個類,我們有其他選擇。早些時候,我們看到了一個 handler 被用于協助 AsyncTask 。事實證明,使用 handler 同樣適用于這種情況。我們需要做的一個程序執行這項工作,再細分成碎片。我們先創建一個 handleMessage ,它可以處理各種任務。使用它的時候,當某部分代碼執行完成時,它需要一個在類中的 handler 發送一個消息告訴它運行下一段代碼:
// MyTaskModule.java
private static class LoginHandler extends Handler {
MyCallback callback;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case QUIT:
callback.onFailure();
break;
case STARTED:
doStuff();
send(obtainMessage(NEXT_STEP));
break;
case NEXT_STEP:
callback.onSuccess();
break;
}
}
}
開始時,我們給這個 handler 發送一個開始執行的消息。運行該代碼后,它會發送一個 NEXT_STEP 消息,使得代碼進入到下一步對應的 case 當中。另外,我們還給出了退出的方式,通過發送一個消息稱為 QUIT 來進行退出。這將使我們能夠脫離 handler,并調用一些代碼,關閉整個事情。
為了向你展示如何工作的,我舉例我的一個叫做 MangoLoginHandler 的例子。這實際上是 handler 的一個實例或子類。我們所有的工作都是在 handleMessage 里面做。如果我們想要的話,我們的代碼可以被分割成更多的方法。在我底下的代碼,我有一個模塊與一個公共的方法,讓我們能夠從 Service 運行代碼發送 LOOPER_STARTED 消息。這是我們要獲得數據必要做的一步。當我們得到一個 handler 的引用的時候,這個 handler 是可以處于任何我們調用它的線程。因為我打算在 IntentService 中來啟動,這時候我就不是在主線程。所有這一切都封裝運行在后臺線程中。
就像一個巨大的代碼墻,我認為它仍然可以更好地分割成單獨的方法。從閱讀我的 handler 的標簽,我覺得我有一個更好的代碼。
Activity 到 Service 到 Module(25:57)
還有一個點,我還沒有說呢。我們可以在一個 Activity 和 Service 之間進行交流。我們知道,我們可以創建一個任務模塊,把一個 handler 放到里面,并從 Service 啟動模塊。但是,現在缺少的部分是能夠從 handler 回到了 Service 和 Activity 的一部分。
你所能做的就是在任務模塊中定義一個非常簡單的接口。這個接口可以是僅限內部訪問的,也可以公開。然后,你可以在啟動該 handler 的 Service 或啟動任務的模塊來實現這個回調接口。最后你需要響應 onSuccess 和 onFailure 這兩個方法。這些方法意味著任務模塊正運行在該服務上。
// MyTaskModule
public interface MyCallback() {
public void onSuccess();
public void onFailure();
}
// rest of task implementation
public void start(MyCallback callback) {
// call onSuccess or
// onFailure here!
}
在你的 Activity 中,你啟動了你的 Service 并且通過 bundle 傳遞一個 ResultReceiver 。 在 Service 中,你首先實現了回調接口,所以 Service 是該接口的一個實例。當你啟動你的任務模塊,它會獲得這個 Service 作為回調。在模塊中,handler 是貫穿所有的步驟,如果一切順利完成,它就會調用接口對象的 onSuccess 方法。在那時,它會通過 ResultReceiver 回調到 Activity 中。所以你可以通過這樣發送一個 OK 結果到 Activity 中。可以停止加載進度圈,并且算是登錄進入程序啦。
總結(28:10)
我們已經創造了四個對象。你有一個 Activity ,管理控制用戶看到的內容。你有一個 Service ,照顧所有那些你的任務。它有一條與 Activity 的交流路線(那個 ResultReceiver )。你還有一個單獨的模塊,是將從 Service 中分離出的,以使其能夠重用。在任務模塊中,您可以有一個 handler,能夠通過全部任務過程,并進行通信,您需要知道所有回到 Activity 的方式,以便用戶可以響應。
進一步閱讀(28:55)
當我在研究這個問題時,我發現這些資源真的很有用。 它們是 CodePath guides . Efficient Android Threading 是一本我讀起來很享受的書. 最后還有, Android 官方文檔 processes and threads , 以及 multiple threads 這篇文章也很有幫助.
問與答(29:30)
問: 我可以使用 Android 的帳戶管理器(Account Manager)來編寫類似的代碼嗎?
Ari: 我很高興你提出這個問題,因為這是從我所展示的代碼中不能清楚的。我們實際上也在使用谷歌的帳戶管理器。在設備上注冊用戶帳戶,就像一個谷歌帳戶,實際上是這個代碼所做的許多任務之一。這實際上不是用來替換帳戶管理器的使用,但是您可以將帳戶數據寫入到帳戶管理系統中,這是整個過程的一部分。
問: 我看到了一個谷歌的演示,建議使用一個同步適配器(Sync Adapter)來進行事情排隊和處理反饋。這一點你們有考慮過嗎?
Ari: 這是合理的。我認為在使用同步適配器時,你必須建立一個適合于你需要做的其他任務的實現。如果是這樣的情況,你不需要執行其他的任務,那么就沒有什么好的理由來使用網絡庫來做同步適配器的工作了。如果調用同步適配器能做你需要的,像同步谷歌帳戶,取決于你自己是否直接調用或包裝(wrapper)。對于這樣的東西,我可能會給它一個非常簡單的包裝。同步谷歌帳戶數據從本地到遠程,那肯定是選擇最合適的東西。
Stephan: 關于同步適配器(Sync Adapter)有大量的樣板代碼。你必須創建自己的內容提供者。但是,它應該為你節省一些工作。這是很好的,但它還不夠好。
問: 你能說得更詳細一點關于使用信號返回到UI線程嗎?特別是,你是否需要鎖?你的回調如何結束更新 activity?
Ari: 我沒有用任何鎖實現這個。基本上,我不想把它寫成這樣一種方式:通過 ResultReceiver 修改對象會出問題。在我的特殊用途的情況下,我避免鎖,我做了兩件事情。首先,我停止了進度圈,這實際上不是線程安全的修改。但是,因為執行是傳遞給用戶界面線程的,所以這還是不錯的。我會在 Activity 中要求一個用戶界面線程的方法來改變這個。我正在做的下一件事是完成登錄活動,這在用戶界面線程上是安全的。我在那個方法中不做太多的事情,避免了這個問題。如果你真的有需要同步更新很多東西,那么我會關注它,然后讓我的需求盡可能小。
問: 你有想過通過一個 EventBus 來分離 Activity 和 Service 嗎?
Ari: 老實說,我沒有,不過我也很愿意將來能談論這一步。
問: 我們如何能直觀地理解,為什么 Android 通過 Service 、Sync Adapter、 AsyncTask 還有 handler 來設計程序?
Stephan: 其實是關乎電池的問題,是因為在手機上沒有風扇來散熱。如果你有很多東西,你把它放在你的口袋里,它會開始燃燒你。而電腦上,內存是如此便宜。你可以做任何你想做的,因為你有很多的內存、一個冷卻風扇,和大量的空間。在安卓,你沒有那個。很多它歸結為電池的問題。
Suyash: 我只是想增加一些有趣的東西。當我第一次進入安卓系統時,你首先嘗試在主線程中做所有的事情。如果你做過 JavaScript 編程,你應該不需要使用多線程,直到進入 Android 開發。你的大腦開始學習如何處理新的線程。使用 Android 系統的 Services , AsyncTask ,甚至 handler。所以,這基本上是因為性能的原因,因為一切不應該發生在用戶界面線程。我認為這是天才的設計,因為我沒有看到它在 iOS。 AsyncTask 很獨特。
Ari: 我的經驗是,有一個非常直觀和手動學習過程中,當你遇到性能問題。我很同情,這要學習大量的信息。Stephan 指的是電池的使用和性能,但我想補充說明。作為一個程序員,如果解決方案是在不同的線程上運行任務,就很容易創建新的線程。然后,你會遇到鎖定問題和線程安全問題。如果你忘記管理自己這些過程的生命周期,那么他們仍然運行在 Android 中。所以,當你開始理解這些概念時,很難,但它實際上是最好的,去學習它們然后提升自己吧。
問: 用戶可能需要等待登錄,所以你處理的是什么方式,讓一個進程運行,同時也響應用戶?
Ari: 在“芒果”應用程序中,我們通過返回按鈕來解決這個問題,這是工具欄頂部的后箭頭。一個對話的消息會問用戶,是否他們可以停留一分鐘的時間。另一種選擇是讓這個直接響應,但要給你的 Activity 一個間隙狀態。我們可以在這樣一種方式,用戶將看到,他們已經能夠返回到應用程序。他們可以看到以前的屏幕,但他們看不到完整的應用程序。然后,我們必須給他們一些反饋,該過程是整理和等待另一分鐘。這樣,如果他們按了回到 home 頁面,登錄過程可以繼續工作。
這是非常重要的,因為我們仍然希望在登錄過程中工作。使用 Service 的好處就是它能夠繼續工作而不容易被殺死。但因為這個潛力,我們也必須小心,我們可能要在用戶界面中做一些事情,而它們可能已經不見了。你可以微調你的頁面讓它們若隱可見,這樣它們就不會被回收,這樣你可以在你的 ResultReceiver 正常做某些事了。我覺得這樣做有點枯燥,我想真的想不出一個更好的解決方案了。
Sign up to be notified of new videos — we won’t email you for any other reason, ever.