日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

社区说|浅谈 WorkManager 的设计与实现:系统概述

發布時間:2024/1/18 windows 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 社区说|浅谈 WorkManager 的设计与实现:系统概述 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

什么是 社區說 ?

反思 系列博客是一種看似 “內卷” ,但卻 效果顯著 的學習方式,該系列起源和目錄請參考 這里 。

困境

作為一名 Android 開發者,即使你沒有用過,也一定對 WorkManager 耳熟能詳。

自2018年發布以來,作為 Google 官方推出的架構組件,它未像 LiveData、ViewModel 一樣廣泛應用。究其原因,一起來看 官方 當初對 WorkManager 的描述:

WorkManager 用于執行可 延遲、異步 的后臺任務。它提供了一個 可靠可調度 的后臺任務執行環境,可以處理 即使在應用退出或設備重啟后仍需要運行 的任務。

快速提煉重點,我們得到了什么?

WorkManager,可以處理后臺任務,這些任務哪怕手機重啟也一定會執行?

看完簡介,我的內心毫無波瀾,“關機重啟仍會執行” 的確很不錯,but who cares ? 我根本用不到這些。

它給人的第一印象并不驚艷,甚至可以說 平平無奇 ,無論是相親市場還是技術領域,這都非常致命。

經過一系列的實踐和研究后,回過頭再看 WorkManager,筆者認為 WorkManager 是傳統 Android 領域內學院派編程風格的代表作,是滄海遺珠。它為 后臺任務的處理和調度 提供了一個優秀的解決方案。

即使如此,WorkManager 仍和 Paging 面臨著同樣的 困境: 難以推廣、默默無聞。簡單的項目用不到,復雜的項目經過若干年的沉淀,該領域早已應用了其它方案,學習和遷移成本過高,以至不被需要。

——時至今日,社區內除了若干 使用簡介源碼分析 的博客單篇,我們仍很難找到其 實戰進階最佳實踐 的相關系列。

目的

本文筆者將通過針對 Android 的后臺任務管理機制,進行一個系統性的分析和設計。

最終的目的,并非是讓讀者將 WorkManager 強行引入和使用,而是對 后臺任務的管理和調度工具 (后文簡稱 后臺任務庫 )有一個清晰的認知——即使從未使用過,在將來的某一天,遇到類似的業務訴求時,也能快速形成一個清晰的思路和方案。

基本概念

想要構建一個優秀的后臺任務庫,需要依靠不斷的迭代、優化和擴展,最終成為一個靈活、完善的工具。

那么 后臺任務庫 需要提供哪些功能,為什么要設計這些功能?

第一個映入眼簾的概念是:線程調度,顧名思義,它是后臺異步任務的基石。

1.線程調度

舉個例子,你負責的是一個視頻類APP的研發,最初的業務訴求如下:

APP 的日志上報。

需求清晰明了,顯然,日志上報是一個 后臺異步任務,在子線程進行 IO 操作: Log 文件本地的讀寫,以及上傳到服務器。

這里我們引入 任務執行器(TaskExecutor)的概念:其用于執行后臺任務的具體邏輯。通常,我們使用 線程池 來管理任務的線程,并提供線程的 創建、銷毀、調度 等功能:

// androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor public class WorkManagerTaskExecutor implements TaskExecutor {final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());// 1.可切換主線程private final Executor mMainThreadExecutor = new Executor() {@Overridepublic void execute(@NonNull Runnable command) {mMainThreadHandler.post(command);}};// 2.可切換后臺工作線程private final Executor mBackgroundExecutor = Executors.newFixedThreadPool(Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)),createDefaultThreadFactory(isTaskExecutor)); }

為提高可讀性,本文的示例代碼都有 大幅精簡 ,讀者應盡量避免「只見樹木不見森林」,以理解設計理念為主。

這里我們為組件提供了最基礎的線程調度的能力,便于內部實現 主線程后臺線程 的切換。其中我們為后臺線程申請了一個線程池,并根據可用處理器核心數量來設置適當的線程數(通常同時最多執行的任務數為4),以充分利用設備的性能。

2.任務隊列和串行化

接下來我們設計一個任務隊列,保證可以不斷接收新的任務,并及時分發給空閑的后臺線程執行:

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E> {public boolean add(E e);public boolean offer(E e);public E remove();public E poll(); }

很經典的一個隊列接口,WorkManager 也并未單獨造輪子,而是借用了 Java 的 ArrayDeque 類。這是一個非常經典的實現類,在諸多大名鼎鼎的工具庫中都有它的身影(比如RxJava),限于篇幅不展開,感興趣的讀者可自行查看源碼。

讀者可預見到的是,后臺任務的創建和添加的時機是無法控制的,加上 ArrayDeque 設計之初都并未考慮線程同步,因此目前的設計將會有 線程安全問題 。

因此,后臺任務的入列和執行,必須借用一個新的角色保證串行,就像單線程一樣。

實現方案非常簡單,通過代理和加鎖,提供一個TaskExecutor的包裝類即可:

public class SerialExecutorImpl implements SerialExecutor {// 1. 任務隊列private final ArrayDeque<Task> mTasks;// 2. 后臺線程的Executor,即上文中數量為4的線程池private final Executor mBackgroundExecutor;// 3.鎖對象@GuardedBy("mLock")private Runnable mActive;@Overridepublic void execute(@NonNull Runnable command) {// 不斷地入列、出列和任務執行synchronized (mLock) {mTasks.add(new Task(this, command));if (mActive == null) {if ((mActive = mTasks.poll()) != null) {mExecutor.execute(mActive);}}}} }

3.任務狀態、類型和結果回調

下一步,我們對所關注的任務狀態進行一個羅列,不難得出,我們關注的狀態大致有:任務開始(Enqueued)、任務執行中(Running)、任務成功(Successded)、任務失敗(Failed)、任務取消(Cancelled)等幾種:

請注意,上文中的日志上報我們定義成了 一次性工作,即只執行一次,執行結束即永久結束。

實際上,一次性工作 并不能涵蓋所有的業務場景,舉例來說,作為一個視頻類的 APP,我們需 定期 對用戶的播放進度進行一次記錄或上報,保證用戶即使殺掉APP,下次仍在最后記錄的播放位置繼續播放。

這里我們引入了 定期任務 的概念,它只有一個終止狀態 CANCELLED。這是因為定期工作永遠不會結束。每次運行后,無論結果如何,系統都會重新對其進行調度。

最后,我們將后臺任務的執行抽象為一個接口,開發者實現 doWork 接口,實現具體后臺業務,如日志上報等,并返回具體的結果:

public abstract class Worker {// 后臺任務的具體實現,`WorkerManager`內部進行了線程調度,執行在工作線程.@WorkerThreadpublic abstract @NonNull Result doWork(); }public abstract static class Result {@NonNullpublic static Result success() {return new Success();}@NonNullpublic static Result retry() {return new Retry();}@NonNullpublic static Result failure() {return new Failure();} }

持久化

目前為止,我們實現了一個簡化版、基于內存中隊列的后臺任務庫,接下來我們將針對 后臺任務持久化 的必要性,進行進一步的討論。

1.非即時任務

第一步我們需要引入 非即時任務 的概念。

作為互聯網的從業者,讀者對類似的提示彈窗應該并不陌生:

您手機/PC的新版本已下載完畢:「立即安裝」、「定時安裝」、「稍后提醒我」

顯然,這是一個常見的功能,用戶選擇后,應用或系統的后臺需在未來的某個時間點,升級或提醒用戶。如果和前文中的任務類型進行區分,前者我們可以歸納為 即時任務,后者則可稱之為 延時任務非即時任務。

非即時任務的訴求,將會使我們現有 后臺任務庫復雜度呈指數級提升 ,但這是 必要 的,原因如下:

首先,上文中 定期任務 也屬于非即時任務的范疇,雖然該任務是立即執行并等待的,但實際上其真正的業務邏輯,仍是未來的某個時間點觸發;其次,也是最重要的一點,作為一個健壯的后臺任務庫,和 即時任務 相比,對 非即時任務 提供支持的優先級要高得多。

——這似乎違反直覺,在日常開發中, 即時任務 似乎才是主流,但我們忽視了一個事實,資源并非無限

在文章的開始,我們構建了基本的線程調度能力,并創建了一個數量為 4 的線程池。但隨著業務復雜度的提升,線程池可能會同時執行多個任務,這意味著部分晚入列、或優先級低的任務,會經常性等待前面的任務執行完畢。

嚴格意義上講,此時,即時任務都轉化為了非即時任務,再進一步抽象,所有即時任務都是非即時任務。

萬物皆可異步,是異步編程的一個經典概念,該思想在 Handler、RxJava 或 協程 中都有體現。

即時任務被延時執行是合理的嗎?對于后臺任務而言,是非常合理的,如果開發者有明確的訴求, 必須立即 執行某段業務邏輯,那么就不應該用 后臺任務庫,而是直接在內存中調用這塊代碼。

2.持久化存儲

當后臺任務可能被延時執行,思考下一個問題,如何保證任務執行的可靠性?

終極解決方案必然是 后臺任務持久化,通過本地文件或者數據庫存儲后,即使進程被用戶殺掉或系統回收,在合適的時機,APP總是能夠將任務恢復并重建。

考慮到安全性,WorkManager 最終選擇使用了 Room 數據庫,并且設計維護了一個非常復雜的 Database,簡單羅列下核心的 WorkSpec 表:

@Entity(indices = [Index(value = ["schedule_requested_at"]), Index(value = ["last_enqueue_time"])]) data class WorkSpec(// 1.任務執行的狀態,ENQUEUED/RUNNING/SUCCEEDED/FAILED/CANCELLED@JvmField@ColumnInfo(name = "state")var state: WorkInfo.State = WorkInfo.State.ENQUEUED,// 2.Worker的類名,便于反射和日志打印@JvmField@ColumnInfo(name = "worker_class_name")var workerClassName: String,// 3.輸入參數@JvmField@ColumnInfo(name = "input")var input: Data = Data.EMPTY,// 4.輸出參數@JvmField@ColumnInfo(name = "output")var output: Data = Data.EMPTY,// 5.定時任務@JvmField@ColumnInfo(name = "initial_delay")var initialDelay: Long = 0,// 6.定期任務@JvmField@ColumnInfo(name = "interval_duration")var intervalDuration: Long = 0,// 7.約束關系@JvmField@Embeddedvar constraints: Constraints = Constraints.NONE,// ... )

設計好字段后,接下來我們設計其操作類 WorkSpecDao :

@Dao interface WorkSpecDao {// ...@Insert(onConflict = OnConflictStrategy.IGNORE)fun insertWorkSpec(workSpec: WorkSpec)@Query("SELECT * FROM workspec WHERE id=:id")fun getWorkSpec(id: String): WorkSpec?@Query("SELECT id FROM workspec")fun getAllWorkSpecIds(): List<String>@Query("UPDATE workspec SET state=:state WHERE id=:id")fun setState(state: WorkInfo.State, id: String): Int@Query("SELECT state FROM workspec WHERE id=:id")fun getState(id: String): WorkInfo.State?// ... }

細心的讀者會發現,WorkSpecDao 的設計中除了聲明常規的 Insert、Query、Update 等,并沒有 DELETE 類型的操作。

讀者經過認真考慮后,可得該設計是合理的——由于有 setState() 可更新任務的狀態,已完成或取消的工作無需刪除,而是通過 SQL 語句,靈活分類按需獲取,如:

@Dao interface WorkSpecDao {// ...// 獲取全部執行中的任務@Query("SELECT * FROM workspec WHERE state=RUNNING")fun getRunningWork(): List<WorkSpec>// 獲取全部近期已完成的任務@Query("SELECT * FROM workspec WHERE last_enqueue_time >= :startingAt AND state IN COMPLETED_STATES ORDER BY last_enqueue_time DESC")fun getRecentlyCompletedWork(startingAt: Long): List<WorkSpec>// ... }

這可稱之額外收獲,通過 Room 的持久化存儲,在保證了任務能夠被穩定執行的同時,還可對所有任務進行備份,從而向開發者提供更多額外的能力。

準確來說,WorkManager 內部的 Dao 提供了 Delete 方法,但并未直接暴露給開發者,而是用于內部解決 任務間的沖突 問題,這個后文再提。

優先級管理

下面我們針對任務的 優先級 進一步進行討論。

雖然上文明確說了,對于需要立即執行的行為,不應該作為后臺任務,而是應該直接執行對應的業務代碼塊——看起來優先級機制并非剛需。

但實際上,這種機制仍然有一定的必要性。

1.約束條件

說到 約束條件,熟悉 JobScheduler 的開發者對此不會感到陌生,

約束條件概念
NetworkType約束運行工作所需的網絡類型。例如 Wi-Fi (UNMETERED)。
BatteryNotLow如果設置為 true,那么當設備處于“電量不足模式”時,工作不會運行。
RequiresCharging如果設置為 true,那么工作只能在設備充電時運行。
DeviceIdle如果設置為 true,則要求用戶的設備必須處于空閑狀態,才能運行工作。在運行批量操作時,此約束會非常有用;若是不用此約束,批量操作可能會降低用戶設備上正在積極運行的其他應用的性能。
StorageNotLow如果設置為 true,那么當用戶設備上的存儲空間不足時,工作不會運行。

WorkManager 也提供了類似的概念,實際上內部也正是基于 JobScheduler 實現的,但 WorkManager 并非只是單純的代理。

首先,當API版本不足時,WorkManager 兼容性使用 AlarmManager 或 GcmScheduler 作為補充:

// androidx.work.impl.Schedulers.java static Scheduler createBestAvailableBackgroundScheduler(@NonNull Context context,@NonNull WorkManagerImpl workManager) {Scheduler scheduler;if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {scheduler = new SystemJobScheduler(context, workManager);} else {scheduler = tryCreateGcmBasedScheduler(context);if (scheduler == null) {scheduler = new SystemAlarmScheduler(context);}}return scheduler; }

其次,讀者知道,JobScheduler 的 onStartJob 等回調默認運行在主線程,不能直接進行耗時操作。WorkManager 的 Worker 內部則進行了線程調度,默認實現在 WorkerThread:

// androidx.work.impl.background.systemjob.SystemJobService public class SystemJobService extends JobService {public boolean onStartJob(@NonNull JobParameters params) {//...mWorkManagerImpl.startWork(...);} }// androidx.work.impl.WorkManagerImpl public class WorkManagerImpl extends WorkManager {public void startWork(...) {mWorkTaskExecutor.executeOnTaskThread(new StartWorkRunnable(...));} }

由于 JobScheduler 是由系統服務中的 JobSchedulerService 實現的,因此其自身的高權限,可以在APP被殺或重啟后,仍然可以喚起并執行 JobService 及對應的任務。

2.新的?;顧C制?

Android 官方提供了后臺作業的強大支持,國內廠商大多數第一時間卻想拿它來做 ?;?/strong>。

——舉例來說,既然 WorkManager 支持定期任務,且即使 APP 被殺或者重啟都能夠保證執行;那么我一個 IM APP,每 10 秒拉取下接口看有沒有新消息,順便啟動下 APP 某些頁面或者通知組件,想必也是非常合理的。


實際上,對于 ?;?/strong> 的訴求,WorkManager 做不到,其本質是 JobScheduler 做不到:

首先,隨著版本的迭代,Android 系統對后臺任務的管理愈發嚴苛,小于 15 分鐘的定期任務已經被強制調整為 15 分鐘執行,避免頻繁的后臺定時任務對前臺應用的影響,規避了 API 的非法濫用:

WorkManager : 我把你當兄弟,你竟然想睡我?

其次是筆者的猜測,由于用戶安全等相關的考量,國內設備廠商對 JobSchedulerService 等相關都有一定的魔改——比如,當用戶手動將 APP 強制關閉,這種操作意圖擁有最高優先級,即使是系統服務也不應對其再次啟動(廠商白名單除外,如微信和QQ?)。

兩點結合,WorkManager 的定期任務受到了嚴格的限制,這也意味著類似保活需求其無法滿足,其 “不穩定” 性這也是其國內應用較少的主要原因之一。

3.前臺服務

最后,我們討論下 如何規范地調度系統資源

最經典的場景仍然是后臺任務的加急,即使有約束條件,部分后臺任務仍需要被 特殊加急 ,比如 用戶聊天時發送短視頻、處理付款或訂閱流程 等。這些任務對用戶很重要,會在后臺快速執行,并需要立即開始執行。

系統對于應用的資源分配非常嚴格,讀者可以通過 這里 簡單了解。

由于任務的執行依賴于系統對資源的分配,因此想要提高執行的優先級,勢必需要提升 APP 組件自身的優先級,那么實現方案已經非常明顯了:使用 前臺服務

當你需要通過調用類似 worker.setExpedited(true) 標記為加急任務時,Worker 內部的具體實現,需開發者額外創建前臺通知,提升優先級的同時,將你后臺的任務行為同步給用戶。

小結

小結并不是總結,還有更多內容可以擴展,比如:

  • 1、Android 系統的 JobScheduler 任務調度機制的內部原理是什么樣的?

  • 2、WorkManager 組件的初始化機制、持久化機制等,內部的實現細節有哪些需要注意的?

  • 3、WorkManager 中的約束任務和非約束任務在執行上有什么不同?

  • 4、WorkManager 還有哪些其它的亮點?

篇幅原因,這些問題筆者將另起一篇進行更深入性的討論,敬請期待。


關于我

Hello,我是 卻把清梅嗅 ,如果您覺得文章對您有價值,歡迎 ??,也歡迎關注我的 博客 或者 GitHub。

如果您覺得文章還差了那么點東西,也請通過 關注 督促我寫出更好的文章——萬一哪天我進步了呢?

  • 我的Android學習體系
  • 關于文章糾錯
  • 關于知識付費
  • 關于《反思》系列

總結

以上是生活随笔為你收集整理的社区说|浅谈 WorkManager 的设计与实现:系统概述的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。