現代 WorkManager API 已釋出

Android開發者發表於2022-02-22

隨著裝置效能提升和軟體生態發展,越來越多的 Android 應用需要執行相對更復雜的網路、非同步和離線等任務。例如使用者想要離線觀看某個視訊,又不想一直停留在應用介面等待下載完成,那麼就需要以一定的方式讓這些離線的過程在後臺執行。再比如您想將一段精彩的 Vlog 分享到社交媒體,肯定也會希望視訊上傳時不會影響到自己繼續使用裝置。這就涉及到了我們今天分享的主題: 使用 WorkManager 管理後臺和前臺工作。

如果您更喜歡通過視訊瞭解此內容,請在此處檢視:

https://www.bilibili.com/vide...

△ 現代 WorkManager API 已釋出

本文將著重探討 WorkManager 的 API 以及用法,幫助您深入瞭解它的執行機制,以及在實際開發中的使用方式。近期也將會有另一篇關於在 Android Studio 中如何更好地使用 WorkManager 的文章,敬請關注。

WorkManager 基礎 API

從首個穩定版本釋出以來,WorkManager 提供了一些基礎 API,幫助您定義工作、放入佇列、依次執行,且在工作完成時通知您的應用。以功能劃分分類,這些基礎 API 包括:

延遲執行

最初的版本中,這些工作只能被定義為延遲執行,也就是它們會在定義之後延期再開始執行。通過這種延期執行策略,一些不緊急或優先順序不高的任務將會推後執行。

WorkManager 的延期執行會充分考慮裝置的低電耗狀態,以及應用的待機儲存分割槽,因此您不必考慮工作需要在哪個具體時間被執行,這些都交給 WorkManager 考慮即可。

工作約束

WorkManager 支援對給定工作執行設定約束條件,約束 可確保將工作延遲到滿足最佳條件時執行。例如,僅在裝置採用不按流量計費的網路連線時、當裝置處於空閒狀態或者有足夠的電量時執行。您可以專心開發應用的其他功能,將對工作條件的檢查交給 WorkManager。

工作間的依賴關係

我們知道,工作之間是可能存在依賴關係的。比如您正在開發一個視訊編輯應用,當剪輯完成後使用者可能需要分享到社交媒體,於是您的應用需要依次渲染若干個視訊片段,然後將它們一起上傳到視訊服務。這個過程是具有先後次序的,也就是上傳工作依賴渲染工作的完成。

再舉另外一個例子,當您的應用完成與後端同步資料後,也許您希望同步過程中產生的本地日誌檔案被及時清理,或者是將來自後端的新資料填充到本地資料庫中。於是您可以請求 WorkManager 按照順序或者並行執行這些工作,從而實現各個工作之間無縫銜接。而 WorkManager 會在確保所有給定條件都滿足後再執行後續的 Worker

多次執行的工作

很多具備與伺服器同步功能的應用都具有這樣的特點: 應用與後端伺服器的同步往往不是一次性的,它可能是需要多次執行的。比如當您的應用提供線上編輯服務時,一定需要頻繁將本地的編輯資料同步到雲端,這就產生了定期執行的工作。

工作狀態

由於您可以隨時檢查某個工作的狀態,因此對於定期執行的工作而言,整個生命週期是透明的。您可以知道一個工作是處於佇列等待、執行中、阻塞還是已完成狀態。

WorkManager 現代 API

上述的基礎 API 早在我們釋出 WorkManager 的第一個穩定版時就已經提供了。首次在 Android 開發者峰會中談到 WorkManager 時,我們把它看作是管理可延期後臺工作的一個庫。如今從底層的角度來看,這種觀點仍然是成立的。但後來我們又新增了更多新功能,並讓 API 更符合現代規範。

立即執行

現在,當您的應用處於前臺時,您可以請求立即執行某項工作。隨後即便應用被置於後臺,這項工作也不會被中斷,而是繼續進行。所以,即使使用者切換到別的應用去使用,您的應用仍然可以繼續實現為照片新增濾鏡、儲存到本地、上傳等一系列工作。

對於大型應用的開發商來說,他們需要在優化資源使用方面投入更多的資源和精力。但 WorkManager 可以憑藉優秀的資源分配策略大大減輕他們的負擔。

多程式 API

由於使用了新的多程式庫處理工作,WorkManager 引入了新的 API,並進行了底層優化來幫助大型應用更有效地安排和執行工作。這得益於新的 WorkManager 可以在一個獨立的程式中更高效地進行排程和處理。

強化的工作測試 API

應用釋出到商店或是分發給使用者之前,測試是非常重要的一個環節。因此我們增加了 API 來幫助您測試單獨的 Worker 或是一組具備依賴關係的 Worker。

工具改進

在釋出庫的同時,我們還改進了眾多開發者工具。作為開發者,您可以直接使用 Android Studio 來訪問詳盡的除錯日誌和檢查資訊。

開始使用 WorkManager

這些新引入的 API 和改進的工具在為開發者提供更大便利的同時,也促使我們重新思考使用 WorkManager 的最佳時機。雖然從技術角度,我們設計 WorkManager 的核心思想仍然是正確的,但對於日益複雜的開發生態而言,WorkManager 的能力已經大大超過我們的設計預期。

工作的 "持久化" 特性

WorkManager 可以處理您指派給它的任何型別的工作,因此它已經進化成了一個專門處理任務且值得信賴的好工具。WorkManager 在全域性作用域中執行您定義的 Worker,這意味著只要您的應用還在執行,不論是裝置方向的變化,還是 Activity 被回收等,您的工作會被一直留存。不過單憑這一點,還不能稱之擁有 "持久化" 特性,因此 WorkManager 在底層還使用了 Room 資料庫來保證當程式被結束或裝置重啟後,您的工作仍然可以執行,並有可能從中斷位置繼續執行。

執行需要長時間執行的工作

WorkManager 2.3 版本引入了對長時間執行的工作的支援。當我們談到長時間執行的工作時,指的是執行時間超過 10 分鐘執行視窗期的工作。通常情況下,一個 Worker 的執行視窗期被限定為 10 分鐘。為了能實現長時間執行的工作,WorkManager 將 Worker 的生命週期與前臺服務的生命週期捆綁在一起。JobScheduler 和程式內排程程式 (In-Process Scheduler) 仍然能感知到這種工作的存在。

由於前臺服務掌握著工作執行的生命週期,而前臺服務又需要向使用者展示通知資訊,所以我們向 WorkManager 新增了相關的 API。使用者的注意力持續時間是有限的,所以 WorkManager 提供了 API 讓使用者能方便地通過通知停止長時間執行的工作。我們來分析一個長時間執行工作示例,程式碼如下:

class DownloadWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
    fun notification(progress: String): Notification = TODO()
    // notification 方法根據進度資訊生成一條 Android 通知訊息。
    suspend fun download(inputUrl: String,
      outputFile: String,
      callback: suspend (progress: String) -> Unit) = TODO()
    // 定義一個用於分塊下載的方法
    fun createForegroundInfo(progress: String): ForegroundInfo {
      return ForegroundInfo(id, notification(progress))
    }
 
    override suspend fun doWork(): Result {
      download(inputUrl, outputFile) { progress -> 
        val progress = "Progress $progress %"
        setForeground(createForegroundInfo(progress))
      } // 提供了一個 suspend 標記的 doWork 方法,其中呼叫下載方法,並顯示最新進度資訊。
      return Result.success() 
    } //下載完成後,Worker 只需要返回成功即可
}

△ DownloadWorker 類

這裡有一個 DownloadWorker 類,它擴充套件自 CoroutineWorker 類。我們會在這個類當中定義一些輔助方法來簡化我們的工作。首先是一個 notification 方法,它可以根據所給定的進度資訊生成一條 Android 通知訊息。接下來我們要定義一個用於分塊下載的方法,這個方法接受三個引數: 下載檔案的 URL、檔案儲存的本地位置、suspend 回撥函式。每當某個分塊下載狀態變化時,此回撥就會被執行一次。於是,回撥中攜帶的資訊就可以被用來生成一條通知。

有了這些輔助方法,我們就可以將 WorkManager 執行長時間執行工作所需要的 ForegroundInfo 例項儲存起來。ForegroundInfo 是由通知 ID 和通知例項組合構造而成的,請繼續參照上述 CoroutineWorker 類的程式碼示例。

在這段程式碼裡,我們提供了一個 suspend 標記的 doWork 方法,其中呼叫了剛才提到的分塊下載輔助方法。由於每次回撥發生時都會提供一些最新的進度資訊,所以我們可以利用這些資訊來構建通知,並呼叫 setForeground 方法來向使用者顯示這些通知。這裡呼叫 setForeground 的操作正是導致 Worker 長時間執行的原因。下載完成後,Worker 只需要返回成功即可,隨後 WorkManager 會將 Worker 的執行與前臺服務解耦分離、清理通知訊息,並在必要時結束相關的服務。因此我們的 Worker 本身並不需要執行服務管理工作。

終止已提交執行的工作

使用者可能會突然改變主意,比如想要取消某個工作。某個前臺執行服務的通知是無法簡單滑動取消的,此前的做法是為這條通知訊息新增一個動作,當使用者點選時會向 WorkManager 傳送一個訊號,從而按照使用者的意圖終止某項工作。您也可以通過執行加急工作來終止,詳見後文。

fun notification(progress: String): Notification {
  val intent = WorkManager.getInstance(context)
      .createCancelPendingIntent(getId())
  return NotificationCompat.Builder(applicationContext, id)
      .setContentTitle(title)
      .setContentText(progress)
      // 其他一些操作
      .addAction(android.R.drawable.ic_delete, cancel, intent)
      .build()
}

△ 派生自 CoroutineWorker 類的 DownloadWorker 類

首先需要建立一個待處理的 Intent,它可以很方便地取消某項工作。我們需要呼叫 getId 方法來獲取這個工作建立時的工作請求 ID,然後呼叫 createCancelPendingIntent API 建立這個 Intent 例項。當此 Intent 被觸發時,它會向 WorkManager 傳送取消工作的訊號,從而實現取消工作的目的。

接下來就要生成帶有自定義動作的通知訊息了。我們使用 NotificationCompat.Builder 設定通知的標題,然後新增一些文字說明。隨後呼叫 addAction 方法將通知中的 "取消" 按鈕與上一步建立的 Intent 關聯起來。於是,當使用者點選 "取消" 按鈕時,這個 Intent 就會被髮送到當前正在執行這個 Worker 的前臺服務,從而將其終止。

執行加急工作

Android 12 中引入了新的前臺服務限制,當應用在後臺時是無法啟動前臺服務的。因此從 Android 12 開始,呼叫 setForegroundAsync 方法會丟擲 Foreground Service Start Not Allowed Exception (不允許啟動前臺服務) 異常。這種情況下,WorkManager 就派上用場了。WorkManager 2.7 版本中增加了對加急工作 (expedited work) 的支援,所以接下來將會向您介紹如何使用 WorkManager 實現終止已提交執行的工作。

從使用者的角度來說,加急工作是由使用者發起的,因此對使用者而言非常重要。甚至應用不在前臺時,這些工作也需要被啟動執行。比如聊天應用需要下載一條訊息中的附件,或者應用需要處理付款訂閱的流程。在早於 Android 12 的 API 版本中,加急工作都是由前臺服務執行的,而從 Android 12 開始,它們將由加急作業 (expedited job) 實現。

系統以配額的形式限制了加急工作的數量。當應用處於前臺時,加急工作不存在任何配額限制,但是當應用轉到後臺執行時,就必須遵從這些限制。配額的大小取決於應用的待機儲存分割槽和程式重要性 (如優先順序)。從字面意思來看,加急工作就是需要儘快啟動執行的工作,這意味著此類工作對於延遲相當敏感,所以也就不支援設定初始延遲或是定期執行的設定。由於受到配額限制,加急工作也不可以取代長時間執行的工作。當您的使用者想要傳送一條重要資訊時,WorkManager 會盡可能保證這條訊息儘快傳送。

class SendMessageWorker(context: Context, parameters: WorkerParameters): 
  CoroutineWorker(context, parameters) {
  override suspend fun getForegroundInfo(): ForegroundInfo {
    TODO()
  }
    
  override suspend fun doWork(): Result {
    TODO()
  }
}

△ 加急工作示例程式碼

例如,一個同步聊天應用訊息的案例使用了加急工作 API。SendMessageWorker 類擴充套件自 CoroutineWorker,而它的作用是負責從後臺為聊天應用同步訊息。加急工作需要在某個前臺服務的上下文中執行,這很類似於 Android 12 之前版本中的長時間執行的工作。因此我們的 Worker 類還需要實現 getForegroundInfo 介面,方便生成和顯示通知訊息。但是在 Android 12 上 WorkManager 不會顯示其他的通知,這是因為我們定義的 Worker 背後是由加急作業實現的。您需要像平常那樣實現一個 suspend 標記的 doWork 方法。需要注意的是,當您的應用佔用了全部的配額後,加急作業可能會被中斷。因此我們的 Worker 最好能跟蹤某些狀態,以便在重新安排執行時間後能夠恢復執行。

val request = OneTimeWorkRequestBuilder<ForegroundWorker>()
    .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
    .build()
 
WorkManager.getInstance(context)
    .enqueue(request)

△ setExpedited API 示例程式碼

您可以使用 setExpedited API 來安排加急工作,這個 API 會告訴 WorkManager,使用者認為給定的工作請求非常重要。由於所能安排的工作存在配額限制,所以您需要表明當應用的配額用盡時該怎麼處理,有兩種備選方案: 其一是將加急請求變成常規工作請求,其二是在配額耗盡時放棄新的工作請求。

WorkManager 多程式 API

從 2.5 版本開始,WorkManager 對支援多程式的應用進行了若干項改進。如果您需要使用多程式 API,就需要定義 work-multiprocess 工件的依賴項,多程式 API 的目標是在輔助程式中對 WorkManager 的冗餘部分或高開銷部分進行大範圍初始化操作。比如有多個程式在同時獲取統一底層 SQLite 資料庫的事務鎖,這時就會發生 SQLite 爭用;而這種爭用正是我們想要通過多程式 API 減少的。另一方面,我們還想確保程式內排程程式在正確的程式中執行。

為了解 WorkManager 初始化時哪些部分是冗餘的,我們需要清楚它會在後臺執行哪些操作。

單程式的初始化

△ 單程式的初始化過程

首先觀察一下單程式初始化過程。應用啟動後,第一件事是有平臺呼叫 Application.onCreate 方法。隨後在程式生命週期的某個時間點,WorkManager.getInstance 會被呼叫以啟動 WorkManager 的初始化。當 WorkManager 初始化完畢後,我們執行 ForceStopRunnable。這個過程很重要,因為此時 WorkManager 會檢查應用之前是否被強制停止過,它會比較 WorkManager 儲存的資訊與 JobSchedulerAlarmManager 中的資訊,確保作業都被準確編入執行計劃中。同時,我們也可以重新安排此前中斷的某些工作,比如程式崩潰後進行的一些恢復工作。大家都知道,這樣做的開銷非常高,我們需要在多個子系統中比較和協調狀態,但是理想狀態下,這種操作只應該被執行一次。另外需要注意,程式內排程程式只在預設程式中執行。

多程式的初始化

△ 多程式的初始化過程

接著我們再看看如果應用有第二個程式會發生什麼。假如應用有第二個程式,基本上它會重複在第一個程式中完成的各項操作。首先第一個程式如上文那樣初始化,並且由於這是主程式 (primary process),所以程式內排程程式 (In-Process Scheduler) 也會在其中執行。對於第二個程式,我們會重複剛才的過程,再次呼叫 Application.onCreate,然後重新初始化 WorkManager。這意味著,我們將重複在第一個程式中所做的所有工作。

根據前面的分析,您也許會感到疑惑,為什麼還需要再次執行 ForceStopRunable 呢?這是由於 WorkManager 並不知道這些程式中哪一個優先順序較高。如果應用是螢幕鍵盤或者微件 (Widget),那麼主程式可能並不等同於預設程式。另外,輔助程式 (secondary processes) 中也沒有執行程式內排程程式 (因為它不是預設程式)。其實程式內排程程式所在的程式選擇非常重要,由於它不受其他永續性排程器的限制影響,所以調整其所在的程式可以顯著提升資料吞吐量。例如,JobScheduler 的作業上限是 100 個,而程式內排程程式則沒有這個限制。

val config = Configuration.Builder()
    .setDefaultProcessName("com.example.app")
    .build()

△ 指定應用的預設程式示例程式碼

通過 WorkManager 定義主程式

我們來看看如何定義指定的預設程式。首先根據自己的意願設定預設程式的名稱,這通常是應用的軟體包名稱,一旦定義了應用的預設程式,那麼程式內排程程式就會在其中執行。但是輔助程式怎麼辦?有沒有辦法能夠防止在其中再次初始化 WorkManager?事實證明這是可以辦到的。其實我們真正需要的是完全不必初始化 WorkManager。

為了實現這個目標,我們引入了 RemoteWorkManager。這個類需要繫結到指定程式 (主程式),並使用繫結服務將次要程式的所有工作請求轉發到這個指定的主程式。這樣一來,您就可以完全避免所有剛才提到的跨程式 SQLite 爭用,因為從開始到結束只有唯一一個程式在向底層 SQLite 資料庫寫入資料。您可以跟往常一樣在輔助程式中建立工作請求,但是此處應該使用 RemoteWorkManager 而不是 WorkManager。使用 RemoteWorkManager 後,會通過繫結服務繫結到主程式中,並將所有工作請求進行轉發,然後儲存到特定佇列等待執行。您可以通過將 RemoteWorkManager 服務合併到應用的 Android Manifest RXML 中實現這個繫結。

val request = OneTimeWorkRequestBuilder<DownloadWorker>()
    .build()
 
RemoteWorkManager.getInstance(context)
    .enqueue(request)

△ 使用 RemoteWorkManager 示例程式碼

<!-- AndroidManifest.xml -->
<service
    android:name="androidx.work.multiprocess.RemoteWorkManagerService"
    android:exported="false" />

△ Manifest 註冊服務示例程式碼

不同程式中執行 Worker

我們已經瞭解如何通過 WorkManager 定義主程式來避免爭用,但有時候,您也希望能夠在不同的程式中執行 Worker。舉個例子,如果您在某應用的輔助程式中執行機器學習工作流 (ML Pipeline),而且該應用還有專門的介面程式,那麼您可能需要在不同的程式中執行不同的 Worker。比如在輔助程式中隔離執行某個工作,這樣一來即使這個程式內出現錯誤而崩潰也不會導致應用的其他部分癱瘓而整體退出,尤其是要保障介面程式正常工作。要實現在不同程式中執行 Worker,您需要擴充套件 RemoteCoroutineWorker 類。這個類與 CoroutineWorker 類似,擴充套件之後您需要自己實現 doRemoteWork 介面。

public class IndexingWorker(
  context: Context,
  parameters: WorkerParameters
): RemoteCoroutineWorker(context, parameters) {
  override suspend fun doRemoteWork(): Result {
    doSomething()
    return Result.success()
  }
}

△ IndexingWorker 類示例程式碼

由於這個方法是在輔助程式中執行的,我們仍然要定義 Worker 需要與哪個程式繫結。為此,我們還需要在 Android Manifest RXML 中新增一個條目。一個應用可以定義多項 RemoteWorker 服務,每項服務都在獨立的程式中執行。

<!-- AndroidManifest.xml -->
<service
    android:name="androidx.work.multiprocess.RemoteWorkerService"
    android:exported="false"
    android:process=":background" />

△ Manifest 註冊服務示例程式碼

這裡您可以看到,我們為名為 background 的輔助程式新增了新服務。現在,您已經在 RXML 中定義好了服務,還需要進一步在工作請求中指明要繫結的元件名稱。

val inputData = workDataOf(
  ARGUMENT_PACKAGE_NAME to context.packageName,
  ARGUMENT_CLASS_NAME to RemoteWorkerService::class.java.name
)
 
val request = OneTimeWorkRequestBuilder<RemoteDownloadWorker>()
    .setInputData(inputData)
    .build()
 
WorkManager.getInstance(context).enqueue(request)

△ 將 RemoteWork 物件放入佇列示例程式碼

元件名稱是軟體包名和類名的組合,您需要將其新增到工作請求的輸入資料中,然後用這個輸入資料建立工作請求,這樣一來 WorkManager 就知道要繫結哪項服務了。我們照常將工作放入佇列中,當 WorkManager 準備執行這項工作時,它首先根據輸入資料中定義的內容找到繫結的服務,並執行 doRemoteWork 方法。這樣一來,所有複雜繁瑣的跨程式通訊的任務都交給 WorkManager 來處理了。

總結

WorkManager 是應對長執行時間工作的推薦方案,推薦您使用 WorkManager 實現請求和取消長時間執行的工作任務。通過本文了解到如何以及何時使用加急工作 API,如何編寫可靠的高效能多程式應用。希望這篇文章對您有所幫助,下一篇文章將對新的後臺任務檢查器做出簡單介紹,敬請關注!

如需更多資源,請參閱:

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章