透過執行緒提升效能

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

在 Android 上有效利用執行緒能夠幫助您加強應用程式的效能。本頁面將會討論運用執行緒的幾個層面:使用 UI 或主要執行緒、應用程式生命週期和執行緒優先順序之間的關係,以及平台可以幫助管理執行緒複雜度的方法。本頁面會說明這些領域可能會發生的問題,以及如何避免的方式。

主要執行緒

當使用者啟動應用程式時,Android 會建立一個新的 Linux 程序,以及一個執行作業執行緒。這個主要執行緒也稱為 UI 執行緒,會負責螢幕上顯示的所有內容。瞭解執行緒的運作方式,可以讓您在設計應用程式時讓應用程式使用主要執行緒,盡可能追求最優良的效能。

內部原理

主要執行緒的設計原理非常簡單:它只負責從執行緒安全作業佇列獲取並執行作業區塊,直到應用程式終止為止。架構會從多種來源產生部分上述的作業區塊。這些來源包括和生命週期資訊、輸入等使用者事件,或來自其他應用程式和程序的事件相關的回呼。另外,應用程式也能自行明確將區塊加入佇列,不必使用架構。

幾乎所有應用程式執行的程式碼區塊都和事件回呼有所關聯,例如輸入、版面配置加載或繪圖。當事件觸發時,事件發生的執行緒會把事件從執行緒推送到主要執行緒的訊息佇列內。然後,主要執行緒即可處理該事件。

當更新動畫或畫面時,系統會嘗試每隔約 16 毫秒執行一次負責繪製畫面的作業區塊,以便在每秒影格數 60 的情況下順利轉譯。UI/檢視區塊階層必須在主執行緒進行更新,系統才能達成此目標。不過,如果主要執行緒的訊息佇列內含的工作太多或太長,導致主要執行緒完成更新速度過慢,那麼應用程式應該把這項工作移到工作站執行緒。如果主要執行緒無法在 16 毫秒內完成工作區塊的執行作業,使用者可能會發現停頓、延遲,或輸入的 UI 回應度不足等問題。如果主要執行緒封鎖大約五秒的時間,系統就會顯示「應用程式無回應(ANR) 對話視窗,允許使用者直接關閉應用程式。

把太多或太長的工作移出主要執行緒,以免這些工作干擾使用者輸入內容轉譯和回應度的品質及速度,這就是建議您在應用程式採用執行緒作業最主要的理由。

執行緒和 UI 物件參照

Android 檢視物件在設計上並不遵守執行緒安全原則。應用程式應該全面藉由主要執行緒建立、使用及刪除 UI 物件。如果您嘗試不在主要執行緒,而是在執行緒修改或甚至參照 UI 物件,可能會導致發生例外狀況、靜默失敗、停止運作或其他未定義的錯誤行為。

參照會發生的問題可以分為兩種:明確參照和隱含參照。

明確參照

許多非主要執行緒工作的最終目標都是更新 UI 物件。但是,一旦其中一個執行緒在檢視區塊階層存取物件,就可能導致應用程式發生不穩定的情況:如果在其他執行緒參照某物件的時候,有工作站執行緒變更了同一物件的屬性,就會產生未定義的結果。

舉例來說,請設想某應用程式在工作站執行緒上直接參照某 UI 物件。工作站執行緒上的物件可能含有 View 的參照,但是系統在工作完成前就從檢視區塊階層中移除了 View。當這兩種操作同時發生時,參照會把 View 物件留在記憶體內,並為該物件設定屬性。但是使用者看不到這個物件,而應用程式在物件的參照消失後也會刪除這個物件。

再舉一個例子,View 物件內含擁有這些物件的活動的參照。如果系統刪除了這個活動,但是殘留參照此活動且藉由執行緒執行的工作區塊 (不限直接或間接),那麼垃圾收集器在該工作區塊執行結束之前便不會收集此活動。

如果在活動生命週期事件 (例如螢幕旋轉) 發生時透過執行緒執行工作,以上情形就可能會導致發生問題。系統在工作期間完成之前,無法執行垃圾收集。因此,在開始垃圾收集之前,記憶體裡可能會有兩個 Activity 物件。

在這類情況下,我們建議您不要讓應用程式在藉由執行緒執行的工作任務中明確參照 UI 物件。避免使用這種參照方式有助於避免發生這類記憶體流失和執行緒爭用等情形。

應用程式應該一律在主要執行緒更新 UI 物件。您應該制定協商政策,讓多個執行緒可以把工作傳輸回主要執行緒,讓最頂層的活動或片段處理更新實際 UI 物件的工作。

隱含參照

請看以下程式碼片段,就能看到藉由執行緒執行的物件經常會出現的程式碼設計問題:

Kotlin

class MainActivity : Activity() {
    // ...
    inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
        override fun doInBackground(vararg params: Unit): String {...}
        override fun onPostExecute(result: String) {...}
    }
}

Java

public class MainActivity extends Activity {
  // ...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

這個片段的問題在於讓程式碼把執行緒物件 MyAsyncTask 宣告為某活動的非靜態內部類別 (或 Kotlin 的內部類別)。這項宣告會為內含的 Activity 執行個體建立隱含參照。因此,在執行緒工作完成之前,物件都會含有該活動的參照,導致系統需要多等一段時間才能刪除參照活動。然後多出來的這段時間會對記憶體造成更多壓力。

如果想直接解決這個問題,您可以把超載類別執行個體定義為靜態類別,或在各自的檔案中定義,藉此移除隱含參照。

您也可以一律取消並清除適當的 Activity 生命週期回呼裡的背景工作,如 onDestroy。不過,這種方法非常不便,也比較容易出錯。一般來說,活動裡不應該直接出現複雜且並非 UI 的邏輯。而且 AsyncTask 已經淘汰,不推薦您在新的程式碼裡面使用。如果想進一步瞭解您可以使用的並行基本功能詳細資訊,請參閱「Android 的執行緒作業」。

執行緒和應用程式活動生命週期

應用程式生命週期可能會影響應用程式執行緒的運作方式。您可能需要決定應不應該在系統刪除活動後繼續保留執行緒。您也應該要瞭解執行緒優先順序之間的關係,以及活動是在前景或背景執行。

保留執行緒

和活動的生命週期相較,這些活動產生的執行緒可以保留更長的時間。無論系統建立或刪除活動,執行緒都會繼續不斷執行,不過等到沒有活動的應用程式元件時,系統就會一併終止應用程式程序和這些執行緒。在某些情況下,您會需要使用這種額外的保留時間。

假設某活動產生了一系列以執行緒執行的工作區塊,然後系統在工作站執行緒可以執行區塊之前就已經刪除該活動。此時,應用程式該如何處理作用中的區塊呢?

如果區塊打算更新已經不存在的 UI,就沒必要繼續處理這個工作了。舉例來說,如果工作內容是從資料庫載入使用者資訊,然後更新檢視畫面,就不再需要使用執行緒。

相較之下,和 UI 並未完全有關聯的工作封包在這種時候可能就比較實用。在這種情況下,您應該繼續保留執行緒。舉例來說,封包可以等待下載圖片,並快取到磁碟內,然後更新相關聯的 View 物件。雖然物件已不存在,但是為了以防使用者返回已經刪除的活動,能夠下載和快取圖片還是非常有用。

想要手動管理所有藉由執行緒執行的物件的生命週期回應並不容易,可能會變得非常繁瑣。如果您不正確管理這些回應,應用程式就可能會出現記憶體爭用問題和效能問題。藉由組合 ViewModelLiveData,您可以載入資料並在出現變更時收到通知,也不用擔心生命週期。ViewModel 物件能夠解決這個問題。ViewModels 可以跨設定變更繼續維持,讓您可以輕鬆保留檢視資料。如果想進一步瞭解 ViewModels,請參閱 ViewModels 指南,而若想進一步瞭解 LiveData,請參閱 LiveData 指南。如果您也想進一步瞭解應用程式架構,請參閱應用程式架構指南

執行緒優先順序

正如「程序和應用程式生命週期」所說的,應用程式執行緒所接收的優先順序有一部分會視應用程式在應用程式生命週期所處的階段而定。當您在應用程式中建立和管理執行緒時,請務必為執行緒設定優先順序,以便準確控管執行緒、執行緒獲得的優先順序,以及執行緒獲得優先順序的時間。如果優先順序設定得太高,執行緒就可能打斷 UI 執行緒和 RenderThread,導致應用程式遺失影格。而如果設得太低,則可能導致非同步工作 (例如載入圖片) 速度過慢。

每次建立執行緒時都應該呼叫 setThreadPriority()。系統的執行緒排程器會從高優先順序的執行緒開始處理,根據需求排出恰當的優先順序,藉此完成所有工作。一般來說,前景群組裡的執行緒約可佔據 95% 的裝置總執行時間,而背景群組約佔 5%。

系統也會藉由使用 Process 類別為每個執行緒指派專用的優先順序值。

根據預設,系統會將執行緒的優先順序設定為和產生執行緒相同的優先順序和群組成員。不過,應用程式可以使用 setThreadPriority() 明確調整執行緒的優先順序。

Process 類別可以提供一系列常數,以便應用程式用來設定執行緒的優先順序,藉此減少指派優先順序值的複雜度。舉例來說,THREAD_PRIORITY_DEFAULT 代表執行緒的預設值。如果執行緒正在執行較不緊急的工作,則應用程式應將執行緒的優先順序設為 THREAD_PRIORITY_BACKGROUND

應用程式可以使用 THREAD_PRIORITY_LESS_FAVORABLETHREAD_PRIORITY_MORE_FAVORABLE 常數當做設定相關優先順序的增量依據。如果想檢視所有的執行緒優先順序,請看 Process 類別內的 THREAD_PRIORITY 常數。

如果想進一步瞭解如何管理執行緒,請看 ThreadProcess 類別的相關說明文件。

執行緒作業的輔助類別

如果開發人員欲使用 Kotlin 做為主要語言,我們建議使用協同程式。協同程式能提供多種好處,包括不使用回呼即可撰寫非同步程式碼,並可結構化並行範圍、取消及錯誤處理。

這個架構也能提供同樣的 Java 類別和基礎功能,以便進行執行緒作業,如 ThreadRunnableExecutors 類別,並可額外提供其他類別,例如 HandlerThread。詳情請參閱「Android 的執行緒作業」。

HandlerThread 類別

所謂處理常式執行緒其實就是一種長時間執行的執行緒,可從佇列中取得工作並進行作業。

如果要從 Camera 物件取得預覽影格,請考慮以下這個常見的難題。當登錄相機預覽影格的時候,您會在 onPreviewFrame() 回呼中收到這些影格,而這個回呼會在呼叫這個回呼的事件執行緒上叫用。如果在 UI 執行緒叫用這項回呼,那麼處理大量像素陣列的工作就會干擾轉譯程序和事件處理工作。

在本範例中,當應用程式委任 Camera.open() 指令在處理常式執行緒處理工作區塊時,相關的 onPreviewFrame() 回呼不會前往 UI 執行緒,而是該處理常式。所以,如果您想長時間在像素上執行工作,這個方法可能比較實用。

當應用程式使用 HandlerThread 建立執行緒的時候,請務必記得根據該執行緒的工作類型設定優先順序。切記,CPU 只能平行處理少量的執行緒。設定優先順序可以幫助系統在所有執行緒爭奪資源的時候瞭解如何正確處理工作。

ThreadPoolExecutor 類別

某些工作類型可以降低為能夠高度平行處理且四處分工的工作。其中一個例子就是為 800 萬畫素圖片的每一個 8x8 區塊運算濾鏡。這項作業會建立非常大量的工作封包,因此並不適合使用 HandlerThread 類別。

ThreadPoolExecutor 這個輔助類別可以幫助處理這項程序。這個類別能夠管理執行緒群組的建立程序、設定優先順序,並可以管理如何把工作分給這些執行緒處理。隨著工作負載的增減情形,這個類別可以迎合工作負載啟動或刪除更多執行緒。

這個類別也可以幫助應用程式產生最合適數量的執行緒。應用程式建構 ThreadPoolExecutor 物件時,會同時設定執行緒數量的下限和上限。隨著 ThreadPoolExecutor 的工作負載增加,這個類別會考量這個最初設定的執行緒數量上下限,以及待處理的工作數量。ThreadPoolExecutor 會根據這些要素判斷任意時間點需要啟用的執行緒數量。

應該建立多少個執行緒?

雖然您可以在軟體層級建立數百個執行緒,但是這樣做可能會造成效能問題。應用程式需要和背景服務、轉譯器、音訊引擎、網路和其他內容共享有限的 CPU 資源。CPU 一次只能平行處理非常少量的執行緒,一旦超過限制,就會發生優先順序和排程問題。所以,重要的是如何按照工作負載要求建立合宜的執行緒數量。

其實您可以使用一些變數處理這項工作,不過選個值 (例如可以從 4 開始) 和 Systrace 進行測試也是同樣有效的解決方式。您可以反覆測試,直到找到不會發生問題的執行緒數量下限為止。

有關執行緒數量,另一個需要考量的問題就是執行緒需要佔用記憶體。每個執行緒最少都需要消耗 64,000 記憶體。隨著裝置上安裝多個應用程式,這個數值很快就會十分龐大,而這個情形在呼叫堆疊漲幅較大的時候特別明顯。

許多系統程序和第三方程式庫通常會啟動專用的執行緒集區。如果您的應用程式可以重複利用現有的執行緒集區,便能藉由減少記憶體和處理資源爭用的情形改善效能。