本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布
WorkManager詳解
一、回顧一下以前的做法
以前我們在處理后台任務時,一般都是使用Service(含IntentService)
或者線程/線程池
,而Service不受頁面生命周期影響,可以常駐后台,所以很適合做一些定時、延時任務,或者其他一些肉眼不可見的神秘勾當。 在處理一些復雜需求時,比如監聽網絡環境自動暫停重啟后台上傳下載這類變態任務,我們需要用Service結合Broadcast一起來做,非常的麻煩,再加上傳輸進度的回調,讓人想瘋!
當然大量的后台任務過度消耗了設備的電量,比如多種第三方推送的service都在后台常駐,不良App后台自動上傳用戶隱私也帶來了隱私安全問題。
二、谷歌開始專項整頓
- 6.0 (API 級 23) 引入了Doze機制和應用程序待機。當屏幕關閉且設備靜止時, 打盹模式會限制應用程序的行為。應用程序待機將未使用的應用程序置於限制其網絡訪問、作業和同步的特殊狀態。
- Android 7.0 (API 級 24) 有限的隱性廣播和Doze-on-the-go.
- Android 8.0 (API 級 26) 進一步限制了后台行為, 例如在后台獲取位置並釋放緩存的 wakelocks。
尤其在Android O(8.0)中,谷歌對於后台的限制幾乎可以稱之為變態:
Android 8.0 有一項復雜功能;系統不允許后台應用創建后台服務。 因此,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService(),以在前台啟動新服務。 在系統創建服務后,應用有五秒的時間來調用該服務的 startForeground() 方法以顯示新服務的用戶可見通知。 如果應用在此時間限制內未調用 startForeground(),則系統將停止服務並聲明此應用為 ANR。
而且加入了對靜態廣播的限制:
Android 8.0 讓這些限制更為嚴格。 針對 Android 8.0 的應用無法繼續在其清單中為隱式廣播注冊廣播接收器。 隱式廣播是一種不專門針對該應用的廣播。 例如,ACTION_PACKAGE_REPLACED 就是一種隱式廣播,因為它將發送到注冊的所有偵聽器,讓后者知道設備上的某些軟件包已被替換。 不過,ACTION_MY_PACKAGE_REPLACED 不是隱式廣播,因為不管已為該廣播注冊偵聽器的其他應用有多少,它都會只發送到軟件包已被替換的應用。 應用可以繼續在它們的清單中注冊顯式廣播。 應用可以在運行時使用 Context.registerReceiver() 為任意廣播(不管是隱式還是顯式)注冊接收器。 需要簽名權限的廣播不受此限制所限,因為這些廣播只會發送到使用相同證書簽名的應用,而不是發送到設備上的所有應用。 在許多情況下,之前注冊隱式廣播的應用使用 JobScheduler 作業可以獲得類似的功能。
於此同時,官方推薦用5.0推出的JobScheduler
替換Service + Broadcast的方案。
並且在Android O,后台Service啟動后的5秒內,如果不轉為前台Service就會ANR!
三、官方的推薦(qiang zhi)做法
場景 | 推薦 |
---|---|
需系統觸發,不必完成 | ThreadPool + Broadcast |
需系統觸發,必須完成,可推遲 | WorkManager |
需系統觸發,必須完成,立即 | ForegroundService + Broadcast |
不需系統觸發,不必完成 | ThreadPool |
不需系統觸發,必須完成,可推遲 | WorkManager |
不需系統觸發,必須完成,立即 | ForegroundService |
四、WorkManager的推出
WorkManager 是一個 Android 庫, 它在工作的觸發器 (如適當的網絡狀態和電池條件) 滿足時, 優雅地運行可推遲的后台工作。WorkManager 盡可能使用框架 JobScheduler , 以幫助優化電池壽命和批處理作業。在 Android 6.0 (API 級 23) 下面的設備上, 如果 WorkManager 已經包含了應用程序的依賴項, 則嘗試使用Firebase JobDispatcher 。否則, WorkManager 返回到自定義 AlarmManager 實現, 以優雅地處理您的后台工作。
也就是說,WorkManager可以自動維護后台任務,同時可適應不同的條件,同時滿足后台Service和靜態廣播,內部維護着JobScheduler,而在6.0以下系統版本則可自動切換為AlarmManager,好神奇!
五、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 } }
向任務添加參數
-
在Request中傳參:
val data=Data.Builder() .putInt("A",1) .putString("B","2") .build() val request2 = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS) .setInputData(data) .build()
-
在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); }
我們需要關注的只有State
和Data
這兩個屬性,首先看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:如果隊列里面已經存在相同名稱的任務,並且該任務處於掛起狀態,則會緩存新任務。當隊列中所有任務執行完畢后,以這個新任務做為序列的第一個任務。
六、總結
看到這里相信大家對於WorkManager的基本用法已經了解的差不多了吧!筆者對WorkManager的了解也還不夠多,歡迎大家多多留言交流!
另外通過這次對WorkManager的學習,我們也看到官方在代碼里面也仍舊在用一些他自己不推薦使用的東西,比如HashMap
、HashSet
、Enum
等,只許州官放火不許百姓點燈?這很谷歌!其實不是的,所謂萬事無絕對,只要你夠自信,自己做好取舍,掌握平衡,用什么還是由你自己做主!