Android8.0後時代的後臺任務JetPack-WorkManager詳解

本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈

WorkManager詳解

1、回顧一下之前的作法

之前咱們在處理後臺任務時,通常都是使用Service(含IntentService)或者線程/線程池,而Service不受頁面生命週期影響,能夠常駐後臺,因此很適合作一些定時、延時任務,或者其餘一些肉眼不可見的神祕勾當。 在處理一些複雜需求時,好比監聽網絡環境自動暫停重啓後臺上傳下載這類變態任務,咱們須要用Service結合Broadcast一塊兒來作,很是的麻煩,再加上傳輸進度的回調,讓人想瘋!javascript

固然大量的後臺任務過分消耗了設備的電量,好比多種第三方推送的service都在後臺常駐,不良App後臺自動上傳用戶隱私也帶來了隱私安全問題。css

2、谷歌開始專項整頓

  • 6.0 (API 級 23) 引入了Doze機制和應用程序待機。當屏幕關閉且設備靜止時, 打盹模式會限制應用程序的行爲。應用程序待機將未使用的應用程序置於限制其網絡訪問、做業和同步的特殊狀態。
  • Android 7.0 (API 級 24) 有限的隱性廣播和Doze-on-the-go.
  • Android 8.0 (API 級 26) 進一步限制了後臺行爲, 例如在後臺獲取位置並釋放緩存的 wakelocks。

尤爲在Android O(8.0)中,谷歌對於後臺的限制幾乎能夠稱之爲變態:html

Android 8.0 有一項複雜功能;系統不容許後臺應用建立後臺服務。 所以,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService(),以在前臺啓動新服務。 在系統建立服務後,應用有五秒的時間來調用該服務的 startForeground() 方法以顯示新服務的用戶可見通知。 若是應用在此時間限制內未調用 startForeground(),則系統將中止服務並聲明此應用爲 ANR。java

並且加入了對靜態廣播的限制:android

Android 8.0 讓這些限制更爲嚴格。 針對 Android 8.0 的應用沒法繼續在其清單中爲隱式廣播註冊廣播接收器。 隱式廣播是一種不專門針對該應用的廣播。 例如,ACTION_PACKAGE_REPLACED 就是一種隱式廣播,由於它將發送到註冊的全部偵聽器,讓後者知道設備上的某些軟件包已被替換。 不過,ACTION_MY_PACKAGE_REPLACED 不是隱式廣播,由於無論已爲該廣播註冊偵聽器的其餘應用有多少,它都會只發送到軟件包已被替換的應用。 應用能夠繼續在它們的清單中註冊顯式廣播。 應用能夠在運行時使用 Context.registerReceiver() 爲任意廣播(無論是隱式仍是顯式)註冊接收器。 須要簽名權限的廣播不受此限制所限,由於這些廣播只會發送到使用相同證書籤名的應用,而不是發送到設備上的全部應用。 在許多狀況下,以前註冊隱式廣播的應用使用 JobScheduler 做業能夠得到相似的功能。nginx

於此同時,官方推薦用5.0推出的JobScheduler替換Service + Broadcast的方案。數組

而且在Android O,後臺Service啓動後的5秒內,若是不轉爲前臺Service就會ANR!緩存

3、官方的推薦(qiang zhi)作法

場景 推薦
需系統觸發,沒必要完成 ThreadPool + Broadcast
需系統觸發,必須完成,可推遲 WorkManager
需系統觸發,必須完成,當即 ForegroundService + Broadcast
不需系統觸發,沒必要完成 ThreadPool
不需系統觸發,必須完成,可推遲 WorkManager
不需系統觸發,必須完成,當即 ForegroundService

4、WorkManager的推出

WorkManager 是一個 Android 庫, 它在工做的觸發器 (如適當的網絡狀態和電池條件) 知足時, 優雅地運行可推遲的後臺工做。WorkManager 儘量使用框架 JobScheduler , 以幫助優化電池壽命和批處理做業。在 Android 6.0 (API 級 23) 下面的設備上, 若是 WorkManager 已經包含了應用程序的依賴項, 則嘗試使用Firebase JobDispatcher 。不然, WorkManager 返回到自定義 AlarmManager 實現, 以優雅地處理您的後臺工做。安全

也就是說,WorkManager能夠自動維護後臺任務,同時可適應不一樣的條件,同時知足後臺Service和靜態廣播,內部維護着JobScheduler,而在6.0如下系統版本則可自動切換爲AlarmManager,好神奇!ruby

5、WorkManager詳解

1.引入

implementation "android.arch.work:work-runtime:1.0.0-alpha06" // use -ktx for Kotlin 

2.重要的類解析

2.1 Worker

Worker是一個抽象類,用來指定須要執行的具體任務。咱們須要繼承Worker類,並實現它的doWork方法:

class MyWorker:Worker() { val tag = javaClass.simpleName override fun getExtras(): Extras { return Extras(...) //也能夠把參數寫死在這裏 } override fun onStopped(cancelled: Boolean) { super.onStopped(cancelled) //當任務結束時會回調這裏 ... } override fun doWork(): Result { Log.d(tag,"任務執行完畢!") return Worker.Result.SUCCESS } } 
向任務添加參數
  1. 在Request中傳參:

    val data=Data.Builder() .putInt("A",1) .putString("B","2") .build() val request2 = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS) .setInputData(data) .build() 
  2. 在Worker中使用:

    class MyWorker:Worker() { val tag = javaClass.simpleName override fun doWork(): Result { val A = inputData.getInt("A",0) val B = inputData.getString("B") return Worker.Result.SUCCESS } } 

固然除了上述代碼中的方法以外,咱們也能夠重寫父級的getExtras(),並在此方法中把參數寫死再返回也是能夠的。

這裏WorkManager就有一個不是很人性的地方了,那就是WorkManager不支持序列化傳值!這一點讓我怎麼說啊,intent和Bundle都支持序列化傳值,爲何恰恰這貨就不行?那麼若是傳一個複雜對象還要先拆解嗎?

任務的返回值

很相似很相似的,任務的返回值也很簡單:

override fun doWork(): Result { val A = inputData.getInt("A",0) val B = inputData.getString("B") val data = Data.Builder() .putBoolean("C",true) .putFloat("D",0f) .build() outputData = data//返回值 return Worker.Result.SUCCESS } 

doWork要求最後返回一個Result,這個Result是一個枚舉,它有幾個固定的值:

  • FAILURE 任務失敗。
  • RETRY 遇到暫時性失敗,此時可以使用WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit)來重試。
  • SUCCESS 任務成功。

看到這裏我就很奇怪,官方不推薦咱們使用枚舉,可是本身卻一直在用,什麼意思?

2.2WorkRequest

也是一個抽象類,能夠對Work進行包裝,同時裝裱上一系列的約束(Constraints),這些Constraints用來向系統指明什麼條件下,或者何時開始執行任務。

WorkManager向咱們提供了WorkRequest的兩個子類:

  • OneTimeWorkRequest 單次任務。
  • PeriodicWorkRequest 週期任務。
val request1 = PeriodicWorkRequestBuilder<MyWorker>(60,TimeUnit.SECONDS).build() val request2 = OneTimeWorkRequestBuilder<MyWorker>().build() 

從代碼中能夠看到,咱們應該使用不一樣的構造器來建立對應的WorkRequest。

接下來咱們看看都有哪些約束:

  • public boolean requiresBatteryNotLow ():執行任務時電池電量不能偏低。
  • public boolean requiresCharging ():在設備充電時才能執行任務。
  • public boolean requiresDeviceIdle ():設備空閒時才能執行。
  • public boolean requiresStorageNotLow ():設備儲存空間足夠時才能執行。
addContentUriTrigger
@RequiresApi(24) public @NonNull Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants) 

指定是否在(Uri指定的)內容更新時執行本次任務(只能用於Api24及以上版本)。瞄了一眼源碼發現了一個ContentUriTriggers,這什麼東東?

public final class ContentUriTriggers implements Iterable<ContentUriTriggers.Trigger> { private final Set<Trigger> mTriggers = new HashSet<>(); ... public static final class Trigger { private final @NonNull Uri mUri; private final boolean mTriggerForDescendants; Trigger(@NonNull Uri uri, boolean triggerForDescendants) { mUri = uri; mTriggerForDescendants = triggerForDescendants; } 

特麼驚呆了,竟然是個HashSet,而HashSet的核心是個HashMap啊,谷歌聲明不建議用HashMap,固然也就不建議用HashSet,但是官方本身在背地裏面乾的這些勾當啊...

setRequiredNetworkType
public void setRequiredNetworkType (NetworkType requiredNetworkType) 

指定任務執行時的網絡狀態。其中狀態見下表:

|枚舉|狀態| |-|-| |NOT_REQUIRED|不須要網絡| |CONNECTED|任何可用網絡| |UNMETERED|須要不計量網絡,如WiFi| |NOT_ROAMING|須要非漫遊網絡| |METERED|須要計量網絡,如4G|

setRequiresBatteryNotLow
public void setRequiresBatteryNotLow (boolean requiresBatteryNotLow) 

指定設備電池電量低於閥值時是否啓動任務,默認false。

setRequiresCharging
public void setRequiresCharging (boolean requiresCharging) 

指定設備在充電時是否啓動任務。

setRequiresDeviceIdle
public void setRequiresDeviceIdle (boolean requiresDeviceIdle) 

指明設備是否爲空閒時是否啓動任務。

setRequiresStorageNotLow
public void setRequiresStorageNotLow (boolean requiresStorageNotLow) 

指明設備儲存空間低於閥值時是否啓動任務。

給任務加約束:
val myConstraints = Constraints.Builder()
        .setRequiresDeviceIdle(true)//指定{@link WorkRequest}運行時設備是否爲空閒 .setRequiresCharging(true)//指定要運行的{@link WorkRequest}是否應該插入設備 .setRequiredNetworkType(NetworkType.NOT_ROAMING) .setRequiresBatteryNotLow(true)//指定設備電池是否不該低於臨界閾值 .setRequiresCharging(true)//網絡狀態 .setRequiresDeviceIdle(true)//指定{@link WorkRequest}運行時設備是否爲空閒 .setRequiresStorageNotLow(true)//指定設備可用存儲是否不該低於臨界閾值 .addContentUriTrigger(myUri,false)//指定內容{@link android.net.Uri}時是否應該運行{@link WorkRequest}更新 .build() val request = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS) .setConstraints(myConstraints)//注意看這裏!!! .build() 
給任務加標籤分組
val request1 = OneTimeWorkRequestBuilder<MyWorker>()
                .addTag("A")//標籤 .build() val request2 = OneTimeWorkRequestBuilder<MyWorker>() .addTag("A")//標籤 .build() 

上述代碼我給兩個相同任務的request都加上了標籤,使他們成爲了一個組:A組。這樣的好處是之後能夠直接控制整個組就好了,組內的每一個成員都會受到影響。

2.3 WorkManager

通過上面的操做,相信咱們已經可以成功建立request了,接下來咱們就須要把任務放進任務隊列,咱們使用WorkManager

WorkManager是個單例,它負責調度任務而且監放任務狀態。

WorkManager.getInstance().enqueue(request) 

當咱們的request入列後,WorkManager會給它分配一個work ID,以後咱們可使用這個work id來取消或者中止任務:

WorkManager.getInstance().cancelWorkById(request.id) 

注意:WorkManager並不必定能結束任務,由於任務有可能已經執行完畢了。

同時,WorkManager還提供了其餘結束任務的方法:

  • cancelAllWork():取消全部任務。
  • cancelAllWorkByTag(tag:String):取消一組帶有相同標籤的任務。
  • cancelUniqueWork(uniqueWorkName:String):取消惟一任務。

2.4WorkStatus

當WorkManager把任務加入隊列後,會爲每一個WorkRequest對象提供一個LiveData(若是這個東東不瞭解的話趕忙去學)。 LiveData持有WorkStatus;經過觀察該 LiveData, 咱們能夠肯定任務的當前狀態, 並在任務完成後獲取全部返回的值。

val liveData: LiveData<WorkStatus> = WorkManager.getInstance().getStatusById(request.id) 

咱們來看這個WorkStatus到底都包涵什麼,咱們點進去看它的源碼:

public final class WorkStatus { private @NonNull UUID mId; private @NonNull State mState; private @NonNull Data mOutputData; private @NonNull Set<String> mTags; public WorkStatus( @NonNull UUID id, @NonNull State state, @NonNull Data outputData, @NonNull List<String> tags) { mId = id; mState = state; mOutputData = outputData; mTags = new HashSet<>(tags); } 

咱們須要關注的只有StateData這兩個屬性,首先看State:

public enum State { ENQUEUED,//已加入隊列 RUNNING,//運行中 SUCCEEDED,//已成功 FAILED,//已失敗 BLOCKED,//已颳起 CANCELLED;//已取消 public boolean isFinished() { return (this == SUCCEEDED || this == FAILED || this == CANCELLED); } } 

這特麼又一個枚舉。看過代碼以後,State枚舉其實就是用來給咱們作最後的結果判斷的。可是要注意其中有個已掛起BLOCKED,這是啥子狀況?經過看它的註釋,咱們得知,若是WorkRequest的約束沒有經過,那麼這個任務就會處於掛起狀態。

接下來,Data固然就是咱們在任務中doWork的返回值了

看到這裏,我感受谷歌大佬的設計思惟仍是很是之強的,把狀態和返回值同時輸出,很是方便咱們作判斷的同時來取值,而且這樣的設計就能夠達到‘屢次返回’的效果,有時間必定要去看一下源碼,先立個flag!

3. 任務鏈

在不少場景中,咱們須要把不一樣的任務弄成一個隊列,好比在用戶註冊的時候,咱們要先驗證手機短信驗證碼,驗證成功後再註冊,註冊成功後再調登錄接口實現自動登錄。相似這樣類似的邏輯比比皆是,實話說筆者之前都是在service裏面用rxjava來實現的。可是如今service在Android8.0版本以上系統不能用了怎麼辦?固然仍是用咱們今天學到的WorkManager來實現,接下來咱們就一塊兒看一下WorkManager的任務鏈。

3.1鏈式啓動-併發

val request1 = OneTimeWorkRequestBuilder<MyWorker1>().build() val request2 = OneTimeWorkRequestBuilder<MyWorker2>().build() val request3 = OneTimeWorkRequestBuilder<MyWorker3>().build() WorkManager.getInstance().beginWith(request1,request2,request3) .enqueue() 

這樣等同於WorkManager把一個個的WorkRequest enqueue進隊列,可是這樣寫明顯更整齊!同時隊列中的任務是並行的。

3.2 then操做符-串發

val request1 = OneTimeWorkRequestBuilder<MyWorker>().build() val request2 = OneTimeWorkRequestBuilder<MyWorker>().build() val request3 = OneTimeWorkRequestBuilder<MyWorker>().build() WorkManager.getInstance().beginWith(request1) .then(request2) .then(request3) .enqueue() 

上述代碼的意思就是先1,1成功後再2,2成功後再3,這期間若是有任何一個任務失敗(返回Worker.WorkerResult.FAILURE),則整個隊列就會被中斷。

在任務鏈的串行中,也就是兩個任務使用了then操做符鏈接,那麼上一個任務的返回值就會自動轉爲下一個任務的參數!

3.3 combine操做符-組合

如今咱們有個複雜的需求:共有A、B、C、D、E這五個任務,要求AB串行,CD串行,但兩個串之間要併發,而且最後要把兩個串的結果彙總到E。

咱們看到這種複雜的業務邏輯,每每都會嚇一跳,可是牛X的谷歌提供了combine操做符專門應對這種奇葩邏輯,不得不說:谷歌是我親哥!

val chuan1 = WorkManager.getInstance()
    .beginWith(A)
    .then(B) val chuan2 = WorkManager.getInstance() .beginWith(C) .then(D) WorkContinuation .combine(chuan1, chuan2) .then(E) .enqueue() 

4. 惟一鏈

什麼是惟一鏈,就是同一時間內隊列裏不能存在相同名稱的任務。

val request = OneTimeWorkRequestBuilder<MyWorker>().build() WorkManager.getInstance().beginUniqueWork("tag",ExistingWorkPolicy.REPLACE,request,request,request) 

從上面代碼咱們能夠看到,首先與以前不一樣的是,此次咱們用的是beginUniqueWork方法,這個方法的最後一個參數是一個可變長度的數組,那就證實這必定是一根鏈條。

而後咱們看這個方法的第一個參數,要求輸入一個名稱,這個名稱就是用來標識任務的惟一性。那若是兩個不一樣的任務咱們給了相同的名稱也是能夠的,可是這兩個任務在隊列中只能存活一個。

最後咱們再來看第二個參數ExistingWorkPolicy,點進去果真又雙叒是枚舉:

public enum ExistingWorkPolicy { REPLACE, KEEP, APPEND } 
  • REPLACE:若是隊列裏面已經存在相同名稱的任務,而且該任務處於掛起狀態則替換之。
  • KEEP:若是隊列裏面已經存在相同名稱的任務,而且該任務處於掛起狀態,則什麼也不作。
  • APPEND:若是隊列裏面已經存在相同名稱的任務,而且該任務處於掛起狀態,則會緩存新任務。當隊列中全部任務執行完畢後,以這個新任務作爲序列的第一個任務。

6、總結

看到這裏相信你們對於WorkManager的基本用法已經瞭解的差很少了吧!筆者對WorkManager的瞭解也還不夠多,歡迎你們多多留言交流!

另外經過此次對WorkManager的學習,咱們也看到官方在代碼裏面也仍舊在用一些他本身不推薦使用的東西,好比HashMapHashSetEnum等,只許州官放火不準百姓點燈?這很谷歌!其實不是的,所謂萬事無絕對,只要你夠自信,本身作好取捨,掌握平衡,用什麼仍是由你本身作主!

相關文章
相關標籤/搜索