透過執行緒提升效能

在 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 記憶體。隨著裝置上安裝多個應用程式,這個數值會快速增加,尤其是呼叫堆疊大幅擴增時。

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