處理程序和執行緒總覽

應用程式元件啟動時,如果應用程式沒有其他正在執行的元件,Android 系統會為應用程式啟動新的 Linux 程序,並使用單一執行緒。根據預設,相同應用程式的所有元件都會在同一個程序和執行緒中執行,也就是主要執行緒。

如果應用程式元件啟動時,該應用程式已有程序 (因為應用程式的其他元件已啟動),則元件會在該程序中啟動,並使用相同的執行緒。不過,您可以安排應用程式中的不同元件在個別程序中執行,也可以為任何程序建立額外執行緒。

本文將探討 Android 應用程式中的程序和執行緒運作方式。

程序

根據預設,應用程式的所有元件都會在同一個程序中執行,而大多數應用程式不會變更這項設定。不過,如果您發現需要控管特定元件所屬的程序,可以在資訊清單檔案中進行這項操作。

每種元件元素 (<activity><service><receiver><provider>) 的資訊清單項目都支援 android:process 屬性,可指定元件執行的程序。您可以設定這項屬性,讓每個元件在各自的程序中執行,也可以讓部分元件共用程序,其他元件則不共用。

您也可以設定 android:process,讓不同應用程式的元件在相同程序中執行,前提是這些應用程式共用同一個 Linux 使用者 ID,且使用相同憑證簽署。

<application> 元素也支援 android:process 屬性,可用於設定適用於所有元件的預設值。

當其他程序需要資源,且這些程序能更直接地為使用者提供服務時,Android 可能會在某個時間點決定關閉程序。因此,在關閉的程序中執行的應用程式元件也會遭到終止。當這些元件有工作要執行時,系統會再次啟動處理程序。

決定要終止哪些程序時,Android 系統會評估這些程序對使用者的相對重要性。舉例來說,與代管可見活動的程序相比,代管不再顯示於螢幕上的活動的程序更容易關閉。因此,是否要終止程序取決於該程序中執行的元件狀態。

如要瞭解程序生命週期的詳細資料,以及與應用程式狀態的關係,請參閱「程序和應用程式生命週期」。

執行緒

啟動應用程式時,系統會為應用程式建立執行緒,稱為主執行緒。這個執行緒非常重要,因為它負責將事件分派至適當的使用者介面小工具,包括繪圖事件。應用程式幾乎一律會透過這個執行緒,與 Android UI 工具包 android.widgetandroid.view 套件中的元件互動。因此,主執行緒有時也稱為 UI 執行緒。不過在特殊情況下,應用程式的主執行緒可能不是 UI 執行緒。詳情請參閱「執行緒註解」。

系統不會為每個元件執行個體建立個別的執行緒。在相同程序中執行的所有元件都會在 UI 執行緒中例項化,且每個元件的系統呼叫都會從該執行緒分派。因此,回應系統回呼的方法 (例如 onKeyDown(),用於回報使用者動作,或是生命週期回呼方法) 一律會在程序的 UI 執行緒中執行。

舉例來說,當使用者觸控畫面上的按鈕時,應用程式的 UI 執行緒會將觸控事件分派給小工具,小工具隨即會設定其按下狀態,並將失效要求發布至事件佇列。UI 執行緒會將要求從佇列中移除,並通知小工具重新繪製自身。

除非您妥善實作應用程式,否則當應用程式因應使用者互動而執行大量工作時,這個單一執行緒模型可能會導致效能不佳。在 UI 執行緒中執行長時間作業 (例如網路存取或資料庫查詢) 會阻斷整個 UI。執行緒遭到封鎖時,無法調度任何事件,包括繪圖事件。

從使用者的角度來看,應用程式似乎停止回應。更糟的是,如果 UI 執行緒遭到封鎖的時間超過幾秒,使用者就會看到「應用程式無回應」(ANR) 對話方塊。使用者可能會決定結束應用程式,甚至解除安裝。

請注意,Android UI 工具包並非安全執行緒。因此,請勿從背景工作執行緒操控 UI。請從 UI 執行緒對使用者介面進行所有操作。Android 單一執行緒模型有兩項規則:

  1. 請勿封鎖 UI 執行緒。
  2. 請勿從 UI 執行緒外部存取 Android UI 工具包。

工作執行緒

由於採用單一執行緒模型,因此請務必避免封鎖 UI 執行緒,確保應用程式 UI 的回應速度。如果您要執行的作業並非即時作業,請務必在個別的背景工作執行緒中執行。請注意,您無法從 UI 或主要執行緒以外的任何執行緒更新 UI。

為協助您遵守這些規則,Android 提供多種從其他執行緒存取 UI 執行緒的方式。以下列出幾種方法:

下列範例說明如何將工作卸載至背景執行緒,並在工作完成後更新 UI 執行緒:

Kotlin

// Kotlin coroutines implementation.
fun onClick(v: View) {
    // Launch a coroutine in the lifecycle scope (e.g., in an Activity or Fragment).
    lifecycleScope.launch {
        // Run the blocking task on the IO dispatcher.
        val bitmap = withContext(Dispatchers.IO) {
            BitmapFactory.decodeFile("image.png")
        }
        // Back on the main thread, update the UI.
        imageView.setImageBitmap(bitmap)
    }
}

Java

// Java Executor implementation.
// (executorService is assumed to be defined elsewhere).
public void onClick(View v) {
    executorService.execute(() -> {
        // Run the heavy task on a background thread.
        Bitmap bitmap = BitmapFactory.decodeFile("image.png");

        // Update the View on the UI thread.
        imageView.post(() -> imageView.setImageBitmap(bitmap));
    });
}

這項實作方式是執行緒安全,因為背景作業是從獨立執行緒完成,而 ImageView 一律從 UI 執行緒操控。

不過,隨著作業複雜度增加,這類程式碼可能會變得複雜且難以維護。如要處理與背景工作執行緒的更複雜互動,可以考慮在背景工作執行緒中使用 Handler,處理從 UI 執行緒傳送的訊息。如要完整瞭解如何在背景執行緒中排定工作時程,以及如何與 UI 執行緒通訊,請參閱「背景工作總覽」。

執行緒安全方法

在某些情況下,您實作的方法會從多個執行緒呼叫,因此必須編寫為執行緒安全。

這項原則主要適用於可遠端呼叫的方法,例如繫結服務中的方法。如果對 IBinder 中實作的方法進行呼叫,且該呼叫源自於 IBinder 執行的相同程序,則方法會在呼叫端的執行緒中執行。不過,如果呼叫來自其他程序,方法會在從執行緒集區中選取的執行緒中執行,而系統會在與 IBinder 相同的程序中維護執行緒集區。不會在程序的 UI 執行緒中執行。

舉例來說,服務的 onBind() 方法是從服務程序的 UI 執行緒呼叫,而物件中實作的方法 (例如實作遠端程序呼叫 (RPC) 方法的子類別) 則是從集區中的執行緒呼叫。onBind()由於服務可以有多個用戶端,因此多個集區執行緒可以同時參與相同的 IBinder 方法,因此 IBinder 方法必須實作為執行緒安全。

同樣地,內容供應器可以接收來自其他程序的資料要求。ContentResolverContentProvider 類別會隱藏處理序間通訊 (IPC) 的管理方式詳細資料,但回應這些要求的 ContentProvider 方法 (即 query()insert()delete()update()getType() 方法) 是從內容供應器程序中的執行緒集區呼叫,而不是程序的 UI 執行緒。因為這些方法可能會同時從任意數量的執行緒呼叫,因此也必須實作為執行緒安全。

處理序間通訊

Android 提供使用 RPC 的 IPC 機制,其中方法是由活動或其他應用程式元件呼叫,但會在另一個程序中遠端執行,任何結果都會傳回給呼叫端。這需要將方法呼叫及其資料分解到作業系統可理解的層級,從本機程序和位址空間傳輸到遠端程序和位址空間,然後在該處重新組裝並重新執行呼叫。

然後以相反方向傳輸回傳值。Android 會提供執行這些 IPC 交易的所有程式碼,因此您可以專心定義及實作 RPC 程式設計介面。

如要執行 IPC,應用程式必須使用 bindService() 繫結至服務。詳情請參閱「服務總覽」。