Android WorkManager

1、概述

在 I / O '18中,Google發布了Android Jetpack。它是一組庫,工具和架構指南,可幫助我們快速輕松地構建出色的Android應用程序。在這款Android Jetpack中,Google團隊發布了一個專門用于安排和管理后臺任務的庫。它被稱為“ WorkManager ”。WorkManager會考慮到操作系統電池優化功能(如Doze,待機等)的限制,在任何情況下(包括啟動它的應用已經退出,甚至設備重啟)任然承諾保證執行工作,并且它有自己的數據庫來維護任務。此外,很容易計劃,取消和管理多個工作順序和平行的執行。下圖是一張其總的架構圖:

WorkManager架構圖

2、WorkManager執行流程

整個WorkManager的執行流程如下圖所示:

WorkManager執行流程

① 給WorkManager發送工作請求WorkRequest。

② WorkManager將該請求的相關參數放入WorkManager的數據庫中。

③ WorkManager根據設備版本、是否是前臺任務等情況將請求操作傳遞給JobScheduler或者AlarmManager等部件。

④ 檢查Worker是否滿足約束條件,當滿足約束條件時調用執行Worker。

3、核心類和相關操作

3.1 核心類概述

3.1.1 Worker

Worker是一個抽象類,這個類用來指定具體需要執行的任務。使用時要繼承這個類并且實現里面的doWork()方法,在其中寫具體的業務邏輯。

3.1.2 WorkRequest

代表一項任務請求。一個 WorkRequest對象至少要指定一個Worker類。同時,還可以向WorkRequest對象添加 ,指定任務應運行的環境等。每個人WorkRequest都有一個自動生成的唯一ID, 可以使用該ID來執行諸如取消排隊的任務或獲取任務狀態等操作。WorkRequest是一個抽象類; 有兩個直接子類 OneTimeWorkRequest和 PeriodicWorkRequest。與WorkerRequest相關的有如下兩個類:

① WorkRequest.Builder:用于創建WorkRequest對象的助手類 ,其有兩個子類OneTimeWorkRequest.Builder和 PeriodicWorkRequest.Builder,分別對應兩者創建上述兩種WorkerRequest。

② Constraints:指定任務運行時的限制(例如,“僅在連接到網絡時才能運行”)。可以通過 Constraints.Builder來創建該對象,并在調用WorkRequest.Builder的build()方法之前,將其傳遞 給WorkerRequest。

3.1.3 WorkManager

這個類用來安排和管理工作請求。前面創建的WorkRequest 對象通過WorkManager來安排的順序。 WorkManager調度任務的時候會分散系統資源,做好類似負載均衡的操作,同時會遵循前面設置的對任務的約束條件。

3.1.4 WorkStatus

每一個WorkRequest都會有一個WorkStatus與之對應,里面包含了該任務的許多信息,可以通過WorkManager來獲取包含WorkStatus的LiveData對象。開發者可以通過觀察該LiveData對象來監聽與之對應的任務所處的狀態,并在任務完成后通過調用WorkStatus的getOutputData()方法獲取返回值。

3.2 典型的使用

3.2.1 創建Worker

首先,創建自己的Worker類,并覆蓋它的 doWork()方法,并更具情況返回執行結果狀態。如下所示:


public class MyWorker extends Worker {

    @Override

    public Worker.Result doWork() {

        // 執行業務邏輯

        doSomething();

        // 根據執行情況進行返回,

        // 返回SUCCESS代表任務執行成功

        // 返回FAILURE代表任務執行失敗,并且此時不會再重新執行任務

        // 返回RETRY,WorkManager會在之后再次嘗試執行任務。

        return Result.SUCCESS;

    }

}

3.2.2 創建Constraints 和 WorkerRequest

如果有必要,可以指定任務運行時的限制。例如,想要指定該任務只應在設備閑置并接通電源時運行。之后根據前面創建的Worker創建WorkerRequest,并將任務約束Constraints 傳遞給它:


// 創建一個WorkerRequest的任務約束

Constraints myConstraints = new Constraints.Builder()

    .setRequiresDeviceIdle(true)

    .setRequiresCharging(true)

    // 還有許多其他任務約束可以添加

    .build();

// 創建一個OneTimeWorkRequest  并將上面的任務約束傳給它。

OneTimeWorkRequest myWorkRequest =

                new OneTimeWorkRequest.Builder(CompressWorker.class)

    .setConstraints(myConstraints)

    .build();

3.2.3 WorkManager運行任務并監聽結果

獲取WorkManager并選擇合適的時間來運行任務。如果需要檢查任務狀態,就可以通過WorkManager來獲取WorkerRequest的WorkStatus句柄來獲取WorkStatus對象,同時可以對該對象進行監聽。


WorkManager.getInstance().enqueue(myWorkRequest );

// 通過WorkerRequest的id來獲取其LiveData<WorkStatus>

WorkManager.getInstance().getStatusById(myWorkRequest .getId())

    .observe(lifecycleOwner, workStatus -> {

        //當workStatus狀態改變時候可以根據業務需要進行操作

        if (workStatus != null && workStatus.getState().isFinished()) {

            // 如果任務執行完畢,可以在這里進行操作

        }

    });

3.2.4 取消任務

WorkerRequest排入隊列后,可以取消任務。可以通過id或者tag進行對應任務的取消。WorkManager會盡最大努力取消任務,但這本質上是不確定的,有可能在嘗試取消任務時,任務可能已經在運行了或者已經運行完成了。


UUID compressionWorkId = compressionWork.getId();

WorkManager.getInstance().cancelWorkById(compressionWorkId);

3.3 其他操作

WorkManager 除了上面的一些基本操作外。還提供了一些其他功能,可以讓設置更多精細的請求。

3.3.1 重復執行任務

創建前面WorkRequest的時候用的是OneTimeWorkRequest ,代表其只執行一次,WorkRequest還有另外一個子類 PeriodicWorkRequest,可以用它來定時循環執行任務。要創建循環任務,可以使用 PeriodicWorkRequest.Builder該類創建一個 PeriodicWorkRequest對象,然后PeriodicWorkRequest按照與OneTimeWorkRequest對象相同的方式入隊 。


// 第二個參數是間隔時間,第三個參數是第二個參數的單位

new PeriodicWorkRequest.Builder myPeriodicBuilder =

        new PeriodicWorkRequest.Builder(MyWorker.class, 12,

                                        TimeUnit.HOURS);

// ...如果有必要,在這里可以給builder加上約束..

PeriodicWorkRequest myPeriodicWork = myPeriodicBuilder .build();

WorkManager.getInstance().enqueue(myPeriodicWork );

3.3.2 鏈接任務

有時候可能需要按特定順序運行多個任務。 WorkManager允許開發者創建和排隊指定多個任務的工作序列,以及設置他們相應的運行順序。

例如,假設有三個 OneTimeWorkRequest對象:workA,workB,和 workC。這些任務必須按照A,B,C 順序依次運行。要入隊它們,用WorkManager.beginWith() 方法創建一個序列 ,傳遞第一個OneTimeWorkRequest對象; 該方法會返回一個WorkContinuation對象,可以通過它來依次按順序添加剩余的OneTimeWorkRequest,最后將整個序列排入 WorkContinuation.enqueue():


WorkManager.getInstance()

    // 首先運行A類任務

    .beginWith(workA1, workA2, workA3)

    // 當所有A類任務運行完畢再運行B類任務

    .then(workB)

    // 接著再運行C類任務

    .then(workC1, workC2)

    .enqueue();

也可以通過使用WorkContinuation.combine() 方法連接多個鏈來創建更復雜的序列 。例如,假設要運行如下序列:

鏈式任務

建立這個序列,創建兩個單獨的鏈,然后將它們連接在一起成為第三個鏈:


WorkContinuation chain1 = WorkManager.getInstance()

    .beginWith(workA)

    .then(workB);

WorkContinuation chain2 = WorkManager.getInstance()

    .beginWith(workC)

    .then(workD);

WorkContinuation chain3 = WorkContinuation

    .combine(chain1, chain2)

    .then(workE);

chain3.enqueue();

雖然WorkManager每個子鏈的運行有序,但是chain1 和 chain2之間的運行順序就無法保證了。例如,workB可能在workC之前或之后運行,或者它們可能同時運行。能保證的是每個子鏈中的任務將按順序運行; 也就是說,workB直到workA 完成后才開始。combine()方法還能這么用WorkContinuation.combine(OneTimeWorkRequest, WorkContinuation…)也就是鏈和單個WorkRequest的結合,詳情參見官方文檔:WorkContinuation文檔(要梯子)

3.3.3 唯一工作序列

可以創建一個唯一的工作序列,通過調用函數 beginUniqueWork() 開始而不是beginWith()。每個唯一的工作序列都有一個名字; WorkManager只允許一個具有該名稱的工作序列存在。當創建一個新的唯一工作序列時,WorkManager如果已經有一個待處理的序列具有相同的名字會根據傳入的策略標志不同有如下三種操作:

① KEEP:保留現有序列并忽略新來的序列

② REPLACE:取消現有的序列并將其替換為新序列

③ APPEND:將新序列附加到現有序列,在現有序列的最后一個任務完成后運行新序列的第一個任務。

如果任務不應多次排隊,就可以使用唯一工作序列。例如,如果想把數據同步到網絡上,可以入隊一個名為“同步”的序列,并將策略標志傳入KEEP,這樣在此期間新的同步工作請求就都會被忽略了。

3.2.4 設置標簽

可以為任何WorkRequest對象分配標記字符串來對任務進行分組 。要設置標簽,可調用WorkRequest.Builder.addTag()方法,例如:


OneTimeWorkRequest myWorkRequest=

        new OneTimeWorkRequest.Builder(MyWorker.class)

    .setConstraints(myConstraints)

    .addTag("myWork")

    .build();

WorkManager類提供了幾種實用方法,可以使用特定標簽對所有任務進行操作。例如 WorkManager.cancelAllWorkByTag() 取消具有特定標記的所有任務,WorkManager.getStatusesByTag() 返回具有該標記的所有任務的WorkStatus。

3.3.5 輸入和輸出

為了獲得更大的靈活性,可以將參數傳遞給任務,并讓任務返回結果。傳遞和返回的值是鍵值對。要將參數傳遞給任務,在創建WorkRequest 對象之前調用WorkRequest.Builder.setInputData() 。該方法傳入Data對象,其通過Data.Builder進行創建。Worker類可以通過調用Worker.getInputData()來訪問這些參數 。要輸出返回值,任務調用 Worker.setOutputData(),該方法的參數也是一個Data對象; 可以通過觀察任務的LiveData<WorkStatus>獲得該輸出。這邊還要說一下對于傳入的參數目前只能是一些int boolean String的基本數據和對應的數組結構,而且總大小要小于10k,不能傳入對象。

下面是定義一個Worker 類的例子:


// 定義 Worker 類:

public class MathWorker extends Worker {

    // 定義傳入參數的key:

    public static final String KEY_X_ARG = "X";

    public static final String KEY_Y_ARG = "Y";

    public static final String KEY_Z_ARG = "Z";

    // ...定義輸出參數的key:

    public static final String KEY_RESULT = "result";

    @Override

    public Worker.Result doWork() {

        // 抓取傳入參數和如果設置抓取失敗的默認值

        int x = getInputData().getInt(KEY_X_ARG, 0);

        int y = getInputData().getInt(KEY_Y_ARG, 0);

        int z = getInputData().getInt(KEY_Z_ARG, 0);

        // ...do something..

        int result = ...

        //...設置輸出參數

        Data output = new Data.Builder()

            .putInt(KEY_RESULT, result)

            .build();

        setOutputData(output);

        return Result.SUCCESS;

    }

}

入隊上述的worker類并傳入參數:


// 創建一個Data 對象:

Data myData = new Data.Builder()

    // 傳入輸入值

    .putInt(KEY_X_ARG, 42)

    .putInt(KEY_Y_ARG, 421)

    .putInt(KEY_Z_ARG, 8675309)

    // ...調用build()進行創建:

    .build();

// ..創建并入隊一個任務并給這個任務設置一個輸入參數

OneTimeWorkRequest mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)

        .setInputData(myData)

        .build();

WorkManager.getInstance().enqueue(mathWork);

輸出值會存在對應WorkRequest的WorkStatus中:


WorkManager.getInstance().getStatusById(mathWork.getId())

    .observe(lifecycleOwner, status -> {

        if (status != null && status.getState().isFinished()) {

          int myResult = status.getOutputData().getInt(KEY_RESULT,

                  myDefaultValue));

          // ...根據返回值做相應處理 ...

        }

    });

如果是一個鏈接任務,則前一個任務的輸出可用作鏈中下一個任務的輸入。如果是一個簡單的單任務鏈,前一個任務通過調用setOutputData()返回結果 ,后一個任務通過調用getInputData()來獲取結果 。如果鏈更復雜, 例如,因為幾個任務都將輸出發送到單個后續任務,就需要通過InputMerger來處理了。

3.3.6 合并輸入參數

前面已經說到了,前一個任務的輸出可用作鏈中下一個任務的輸入。但是當遇到如下情況就有個問題:

一個work有多個前置work

如上圖所示,當一個任務有兩個前置任務時,這是后直接使用前面輸出的output Data,就要給它設置InputMerger策略。來對work1 的output 和work2 的output 進行合并。合并策略現在有如下兩種

3.3.6.1 OverwritingInputMerger

這個策略是用來將前面的output 進行覆蓋合并,如果兩個output 有相同的key,則后者會將前者覆蓋,有key 就給input新加一組key,value對。

第一組數據傳入input
第二組覆蓋新增之前的

3.3.6.2 ArrayCreatingInputMerger

這個策略是用來將前面的output 進行全部保留合并,如果兩個output 有相同的key,則會同時保留兩者的數據,有key 就給input新加一組key,value對。如果有相同的key但是是不同的數據類型,則會拋出異常。

合并兩組數據

使用時候給接收數據的WorkRequest傳入對應的策略就好了:


OneTimeWorkRequest mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)

        .setInputMerger(OverwritingInputMerger.class)

        .build();

4、WorkManager的存在意義。

關于WorkManager的使用上面已經討論完畢了,接下來想討論下WorkManager存在的意義。

4.1 WorkManager和Doze模式

將任務在后臺運行,首先會想到Android的四大組件之一的Service。Service官方對其的定義是這么說的:Service是一個應用程序組件,可以在后臺執行長時間運行的操作,并且不提供用戶界面。一般來說如果我們想在后臺開啟一個長時間的操作,最好通過Service來啟動一個線程進行,而不是直接在Activity中啟動,因為當Activity被銷毀時,一旦內存空間不足,這個線程就會很容易被殺死。但通過Service啟動就不一樣了,開發者只要不手動關閉Service,這個線程也就不容易被Android系統殺死了。而這也成為了Service招黑的一個點,開發者如果進行了一個5分鐘讓服務從后臺獲取數據的操作,那么Android的電會很快耗干凈。就是因為開發者對于Android系統的為所欲為,從Android6.0開始,推出了Doze模式:

Doze模式

該模式簡而言之就是在用戶關閉設備屏幕后,Doze模式啟動并禁用網絡,同步,GPS,警報和wifi掃描等操作,禁用這些操作。它一直保持到用戶打開屏幕或連接充電器,同時Android會在Doze模式下每隔一段時間就放開這些禁錮,讓這些禁用的操作能夠集中在這一小段時間內進行(這樣可以減少喚醒次數,每次喚醒手機還是很耗費電量的)。這樣就一方面可以節省電池,一方面也可以保證一些后臺任務有機會執行。很可惜的是這個模式并不兼容Service,為了用戶的電池著想后臺Service要被拋棄了。Android也是這么做的,從Android8.0開始,如果想通過Service的startService()方法創建后臺服務,會拋出IllegalStateException異常。

而WorkManager、JobScheduler等后臺操作則是很完美的兼容了Doze模式。

4.2 WorkManager和其他后臺任務的比較和關系

其實在WorkManager出來之前,已經有許多兼容Doze模式的后臺操作了,比如JobScheduler、Alarm Manager等。其實從最前面我放的一張圖可以看到其實WorkManager可以說是這些后臺調度操作的集大成者。具體關系如下圖:

WorkManager調用其他后臺任務關系

WorkManager支持API14或更高的版本,它會根據設備API級別和應用程序狀態等因素選擇適當的方式來運行任務。如果調用WorkManager的應用程序正在運行,那就直接創建一個新的線程來運行任務。否則看這個任務是不是立刻運行(沒有設置約束條件),是的話就用Alarm manager運行,否則就看API版本是否高于21,高于21的用JobScheduler來運行任務,低于的情況下,如果有Google 服務(中國就沒有!!!)就用Firebase Job Dispatcher,沒有的話用Alarm manager。

從上面的表述,其實可以看到其他符合Doze模式的調度操作,或多或少有不盡如人意的地方,不是API支持的少了(JobScheduler),就是需要Google的服務(Firebase Job Dispatcher),又或者無法支持義約束條件或者自定義重啟策略(WorkManager)。如果沒有WorkManager,如果開發者想開發出健壯的后臺調用程序,就必須自己去做這些處理。那無疑是一件很麻煩的事情。

到此WorkManager存在意義已經梳理的差不多了,首先它好用,前面已經說明了它的各種方便的操作。其次它支持Doze模式,能省電。最后就是能夠根據實際情況,盡可能的兼容各種版本和應用程序狀態。

Tips:說了WorkManager的各種好,但是就目前我自己使用的經驗來講還是有一些不足之處的,比如它不能輸入輸出對象,比如它沒有任務執行進度回調接口(WorkManager團隊說考慮會在之后的版本加上)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容