本篇文章完全轉(zhuǎn)載于微笑的江豚 的博客地址:
https://my.oschina.net/JiangTun
如有問題优俘,請及時聯(lián)系!
前言
以前我們在處理后臺任務(wù)時萤悴,都是使Service(含IntentService)或線程、線程池姨裸。而Service不受頁面生命周期影響,可以常駐后臺香拉,所以很適合做一些定時啦扬、延時任務(wù)中狂,或者其他肉眼看不到的神秘勾當(dāng)凫碌。在處理一些復(fù)雜需求時,比如監(jiān)聽網(wǎng)絡(luò)環(huán)境自動暫停重啟后臺上傳下載這類任務(wù)時胃榕,我們用Service結(jié)合Broadcast一起來做盛险,非常麻煩,再加上傳輸進(jìn)度的回調(diào)勋又,更是讓人抓狂苦掘。
大量的后臺任務(wù)過度的消耗了設(shè)備的電量,比如多種第三方推送的Service都常駐后臺楔壤,不良APP后臺自動上傳用戶隱私帶來了隱私安全問題鹤啡。
谷歌專項整頓
- 6.0 (API 級 23) 引入了 Doze 機(jī)制和應(yīng)用程序待機(jī)。當(dāng)屏幕關(guān)閉且設(shè)備靜止時, 打盹模式會限制應(yīng)用程序的行為蹲嚣。應(yīng)用程序待機(jī)將未使用的應(yīng)用程序置于限制其網(wǎng)絡(luò)訪問递瑰、作業(yè)和同步的特殊狀態(tài)。
- Android 7.0 (API 級 24) 有限的隱性廣播和 Doze-on-the-go.
- Android 8.0 (API 級 26) 進(jìn)一步限制了后臺行為, 例如在后臺獲取位置并釋放緩存的 wakelocks隙畜。
尤其在Android O(8.0)中抖部,谷歌對于后臺的限制幾乎可以稱之為變態(tài):
- Android 8.0 有一項復(fù)雜功能,系統(tǒng)不允許后臺應(yīng)用創(chuàng)建后臺服務(wù)议惰。 因此慎颗,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService(),以在前臺啟動新服務(wù)俯萎。 在系統(tǒng)創(chuàng)建服務(wù)后傲宜,應(yīng)用有五秒的時間來調(diào)用該服務(wù)的 startForeground() 方法以顯示新服務(wù)的用戶可見通知。 如果應(yīng)用在此時間限制內(nèi)未調(diào)用 startForeground()夫啊,則系統(tǒng)將停止服務(wù)并聲明此應(yīng)用為 ANR蛋哭。
而且加入了對靜態(tài)廣播的限制:
- Android 8.0 讓這些限制更為嚴(yán)格。 針對 Android 8.0 的應(yīng)用無法繼續(xù)在其清單中為隱式廣播注冊廣播接收器涮母。 隱式廣播是一種不專門針對該應(yīng)用的廣播谆趾。 例如,ACTION_PACKAGE_REPLACED 就是一種隱式廣播叛本,因?yàn)樗鼘l(fā)送到注冊的所有偵聽器沪蓬,讓后者知道設(shè)備上的某些軟件包已被替換。 不過来候,ACTION_MY_PACKAGE_REPLACED 不是隱式廣播跷叉,因?yàn)椴还芤褳樵搹V播注冊偵聽器的其他應(yīng)用有多少,它都會只發(fā)送到軟件包已被替換的應(yīng)用营搅。 應(yīng)用可以繼續(xù)在它們的清單中注冊顯式廣播云挟。 應(yīng)用可以在運(yùn)行時使用 Context.registerReceiver() 為任意廣播(不管是隱式還是顯式)注冊接收器。 需要簽名權(quán)限的廣播不受此限制所限转质,因?yàn)檫@些廣播只會發(fā)送到使用相同證書簽名的應(yīng)用园欣,而不是發(fā)送到設(shè)備上的所有應(yīng)用。 在許多情況下休蟹,之前注冊隱式廣播的應(yīng)用使用 JobScheduler 作業(yè)可以獲得類似的功能沸枯。
于此同時,官方推薦用5.0推出的 JobScheduler 替換 Service + Broadcast 的方案赂弓。并且在 Android O绑榴,后臺 Service 啟動后的5秒內(nèi),如果不轉(zhuǎn)為前臺 Service 就會 ANR!
官方的推薦(qiangzhi)做法
場景 | 推薦 |
---|---|
需系統(tǒng)觸發(fā)盈魁,不必完成 | ThreadPool + Broadcast |
需系統(tǒng)觸發(fā)翔怎,必須完成,可推遲 | WorkManager |
需系統(tǒng)觸發(fā)杨耙,必須完成赤套,立即 | ForegroundService + Broadcast |
不需系統(tǒng)觸發(fā),不必完成 | ThreadPool |
不需系統(tǒng)觸發(fā)按脚,必須完成于毙,可推遲 | WorkManager |
不需系統(tǒng)觸發(fā),必須完成辅搬,立即 | ForegroundService |
WorkManager的推出
WorkManager 是一個 Android 庫, 它在工作的觸發(fā)器 (如適當(dāng)?shù)木W(wǎng)絡(luò)狀態(tài)和電池條件) 滿足時, 優(yōu)雅地運(yùn)行可推遲的后臺工作唯沮。WorkManager 盡可能使用框架 JobScheduler , 以幫助優(yōu)化電池壽命和批處理作業(yè)脖旱。在 Android 6.0 (API 級 23) 下面的設(shè)備上, 如果 WorkManager 已經(jīng)包含了應(yīng)用程序的依賴項, 則嘗試使用 Firebase JobDispatcher 。否則, WorkManager 返回到自定義 AlarmManager 實(shí)現(xiàn), 以優(yōu)雅地處理您的后臺工作介蛉。
也就是說萌庆,WorkManager 可以自動維護(hù)后臺任務(wù),同時可適應(yīng)不同的條件币旧,同時滿足后臺Service 和靜態(tài)廣播践险,內(nèi)部維護(hù)著 JobScheduler,而在6.0以下系統(tǒng)版本則可自動切換為AlarmManager吹菱,Amazing!
WorkManager詳解
引入
implementation "android.arch.work:work-runtime:1.0.0-alpha06" // use -ktx for Kotlin
implementation "android.arch.work:work-runtime:1.0.0-alpha01"
重要的解析類
- worker
Worker 是一個抽象類巍虫,用來指定需要執(zhí)行的具體任務(wù)。我們需要繼承 Worker 類鳍刷,并實(shí)現(xiàn)它的 doWork 方法:
class MyWorker:Worker() {
val tag = javaClass.simpleName
override fun getExtras(): Extras {
return Extras(...) //也可以把參數(shù)寫死在這里
}
override fun onStopped(cancelled: Boolean) {
super.onStopped(cancelled)
//當(dāng)任務(wù)結(jié)束時會回調(diào)這里
...
}
override fun doWork(): Result {
Log.d(tag,"任務(wù)執(zhí)行完畢占遥!")
return Worker.Result.SUCCESS
}
}
向任務(wù)添加參數(shù)
在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
}
}
當(dāng)然除了上述代碼中的方法之外,我們也可以重寫父級的getExtras()输瓜,并在此方法中把參數(shù)寫死再返回也是可以的瓦胎。
這里WorkManager就有一個不是很人性的地方了,那就是WorkManager不支持序列化傳值尤揣!這一點(diǎn)讓我怎么說啊搔啊,intent和Bundle都支持序列化傳值,為什么偏偏這貨就不行北戏?那么如果傳一個復(fù)雜對象還要先拆解嗎?
任務(wù)的返回值
很類似很類似的负芋,任務(wù)的返回值也很簡單:
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 任務(wù)失敗示罗。
- RETRY 遇到暫時性失敗惩猫,此時可使用WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit)來重試芝硬。
- SUCCESS 任務(wù)成功。
看到這里我就很奇怪轧房,官方不推薦我們使用枚舉拌阴,但是自己卻一直在用,什么意思奶镶?
WorkRequest
也是一個抽象類迟赃,可以對 Work 進(jìn)行包裝,同時裝裱上一系列的約束(Constraints)厂镇,這些 Constraints 用來向系統(tǒng)指明什么條件下纤壁,或者什么時候開始執(zhí)行任務(wù)。
WorkManager 向我們提供了 WorkRequest 的兩個子類:
OneTimeWorkRequest 單次任務(wù)捺信。
PeriodicWorkRequest 周期任務(wù)酌媒。
val request1 = PeriodicWorkRequestBuilder<MyWorker>(60,TimeUnit.SECONDS).build()
val request2 = OneTimeWorkRequestBuilder<MyWorker>().build()
從代碼中可以看到,我們應(yīng)該使用不同的構(gòu)造器來創(chuàng)建對應(yīng)的 WorkRequest。
接下來我們看看都有哪些約束:
public boolean requiresBatteryNotLow ():執(zhí)行任務(wù)時電池電量不能偏低秒咨。
public boolean requiresCharging ():在設(shè)備充電時才能執(zhí)行任務(wù)喇辽。
public boolean requiresDeviceIdle ():設(shè)備空閑時才能執(zhí)行。
public boolean requiresStorageNotLow ():設(shè)備儲存空間足夠時才能執(zhí)行雨席。
addContentUriTrigger
@RequiresApi(24)
public @NonNull Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants)
指定是否在(Uri 指定的)內(nèi)容更新時執(zhí)行本次任務(wù)(只能用于 Api24及以上版本)菩咨。瞄了一眼源碼發(fā)現(xiàn)了一個 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缨硝,當(dāng)然也就不建議用HashSet,可是官方自己在背地里面干的這些勾當(dāng)啊...
setRequiredNetworkType
public void setRequiredNetworkType (NetworkType requiredNetworkType)
指定任務(wù)執(zhí)行時的網(wǎng)絡(luò)狀態(tài)罢低。其中狀態(tài)見下表:
枚舉 | 狀態(tài) |
---|---|
NOT_REQUIRED | 不需要網(wǎng)絡(luò) |
CONNECTED | 任何可用網(wǎng)絡(luò) |
UNMETERED | 需要不計量網(wǎng)絡(luò)查辩,如 WiFi |
NOT_ROAMING | 需要非漫游網(wǎng)絡(luò) |
METERED | 需要計量網(wǎng)絡(luò),如4G |
setRequiresBatteryNotLow
public void setRequiresBatteryNotLow (boolean requiresBatteryNotLow)
指定設(shè)備電池電量低于閥值時是否啟動任務(wù)网持,默認(rèn) false宜岛。
setRequiresCharging
public void setRequiresCharging (boolean requiresCharging)
指定設(shè)備在充電時是否啟動任務(wù)。
setRequiresDeviceIdle
public void setRequiresDeviceIdle (boolean requiresDeviceIdle)
指明設(shè)備是否為空閑時是否啟動任務(wù)
setRequiresStorageNotLow
public void setRequiresStorageNotLow (boolean requiresStorageNotLow)
指明設(shè)備儲存空間低于閥值時是否啟動任務(wù)功舀。給任務(wù)加約束:
val myConstraints = Constraints.Builder()
.setRequiresDeviceIdle(true)//指定{@link WorkRequest}運(yùn)行時設(shè)備是否為空閑
.setRequiresCharging(true)//指定要運(yùn)行的{@link WorkRequest}是否應(yīng)該插入設(shè)備
.setRequiredNetworkType(NetworkType.NOT_ROAMING)
.setRequiresBatteryNotLow(true)//指定設(shè)備電池是否不應(yīng)低于臨界閾值
.setRequiresCharging(true)//網(wǎng)絡(luò)狀態(tài)
.setRequiresDeviceIdle(true)//指定{@link WorkRequest}運(yùn)行時設(shè)備是否為空閑
.setRequiresStorageNotLow(true)//指定設(shè)備可用存儲是否不應(yīng)低于臨界閾值
.addContentUriTrigger(myUri,false)//指定內(nèi)容{@link android.net.Uri}時是否應(yīng)該運(yùn)行{@link WorkRequest}更新
.build()
val request = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS)
.setConstraints(myConstraints)//注意看這里F汲!辟汰!
.build()
給任務(wù)加標(biāo)簽分組
val request1 = OneTimeWorkRequestBuilder<MyWorker>()
.addTag("A")//標(biāo)簽
.build()
val request2 = OneTimeWorkRequestBuilder<MyWorker>()
.addTag("A")//標(biāo)簽
.build()
上述代碼我給兩個相同任務(wù)的request都加上了標(biāo)簽列敲,使他們成為了一個組:A組。這樣的好處是以后可以直接控制整個組就行了帖汞,組內(nèi)的每個成員都會受到影響戴而。
WorkManager
經(jīng)過上面的操作,相信我們已經(jīng)能夠成功創(chuàng)建 request 了翩蘸,接下來我們就需要把任務(wù)放進(jìn)任務(wù)隊列所意,我們使用 WorkManager。
WorkManager 是個單例催首,它負(fù)責(zé)調(diào)度任務(wù)并且監(jiān)聽任務(wù)狀態(tài)扶踊。
WorkManager.getInstance().enqueue(request)
當(dāng)我們的 request 入列后,WorkManager 會給它分配一個 work ID郎任,之后我們可以使用這個work id 來取消或者停止任務(wù):
WorkManager.getInstance().cancelWorkById(request.id)
注意:WorkManager 并不一定能結(jié)束任務(wù)秧耗,因?yàn)槿蝿?wù)有可能已經(jīng)執(zhí)行完畢了。
同時舶治,WorkManager 還提供了其他結(jié)束任務(wù)的方法:
- cancelAllWork():取消所有任務(wù)分井。
- cancelAllWorkByTag(tag:String):取消一組帶有相同標(biāo)簽的任務(wù)胶台。
- cancelUniqueWork(uniqueWorkName:String):取消唯一任務(wù)。
WorkStatus
當(dāng) WorkManager 把任務(wù)加入隊列后杂抽,會為每個WorkRequest對象提供一個 LiveData(如果這個東東不了解的話趕緊去學(xué))诈唬。 LiveData 持有 WorkStatus;通過觀察該 LiveData, 我們可以確定任務(wù)的當(dāng)前狀態(tài), 并在任務(wù)完成后獲取所有返回的值。
val liveData: LiveData<WorkStatus> = WorkManager.getInstance().getStatusById(request.id)
我們來看這個 WorkStatus 到底都包涵什么缩麸,我們點(diǎn)進(jìn)去看它的源碼:
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);
}
我們需要關(guān)注的只有 State 和 Data 這兩個屬性铸磅,首先看 State:
public enum State {
ENQUEUED,//已加入隊列
RUNNING,//運(yùn)行中
SUCCEEDED,//已成功
FAILED,//已失敗
BLOCKED,//已刮起
CANCELLED;//已取消
public boolean isFinished() { return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
}
}
這特么又一個枚舉『贾欤看過代碼之后阅仔,State 枚舉其實(shí)就是用來給我們做最后的結(jié)果判斷的。但是要注意其中有個已掛起 BLOCKED弧械,這是啥子情況八酒?通過看它的注釋,我們得知刃唐,如果 WorkRequest 的約束沒有通過羞迷,那么這個任務(wù)就會處于掛起狀態(tài)。
接下來画饥,Data 當(dāng)然就是我們在任務(wù)中 doWork 的返回值了衔瓮。看到這里抖甘,我感覺谷歌大佬的設(shè)計思維還是非常之強(qiáng)的热鞍,把狀態(tài)和返回值同時輸出,非常方便我們做判斷的同時來取值衔彻,并且這樣的設(shè)計就可以達(dá)到‘多次返回’的效果薇宠,有時間一定要去看一下源碼,先立個 flag艰额!
任務(wù)鏈
在很多場景中澄港,我們需要把不同的任務(wù)弄成一個隊列,比如在用戶注冊的時候悴晰,我們要先驗(yàn)證手機(jī)短信驗(yàn)證碼慢睡,驗(yàn)證成功后再注冊,注冊成功后再調(diào)登陸接口實(shí)現(xiàn)自動登陸铡溪。類似這樣相似的邏輯比比皆是,實(shí)話說筆者以前都是在 service 里面用 rxjava 來實(shí)現(xiàn)的泪喊。但是現(xiàn)在 service 在 Android8.0版本以上系統(tǒng)不能用了怎么辦棕硫?當(dāng)然還是用我們今天學(xué)到的 WorkManager 來實(shí)現(xiàn),接下來我們就一起看一下 WorkManager 的任務(wù)鏈袒啼。
- 鏈?zhǔn)絾?并發(fā)
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 進(jìn)隊列哈扮,但是這樣寫明顯更整齊纬纪!同時隊列中的任務(wù)是并行的。
- then 操作符-串發(fā)
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,這期間如果有任何一個任務(wù)失敯忻怼(返回 Worker.WorkerResult.FAILURE),則整個隊列就會被中斷问畅。
在任務(wù)鏈的串行中,也就是兩個任務(wù)使用了 then 操作符連接六荒,那么上一個任務(wù)的返回值就會自動轉(zhuǎn)為下一個任務(wù)的參數(shù)护姆!
- combine 操作符-組合
現(xiàn)在我們有個復(fù)雜的需求:共有A、B掏击、C卵皂、D、E這五個任務(wù)砚亭,要求 AB 串行灯变,CD 串行,但兩個串之間要并發(fā)捅膘,并且最后要把兩個串的結(jié)果匯總到E柒凉。
我們看到這種復(fù)雜的業(yè)務(wù)邏輯,往往都會嚇一跳篓跛,但是牛X的谷歌提供了combine操作符專門應(yīng)對這種奇葩邏輯膝捞,不得不說:谷歌是我親哥!
val chuan1 = WorkManager.getInstance()
.beginWith(A)
.then(B)
val chuan2 = WorkManager.getInstance()
.beginWith(C)
.then(D)
WorkContinuation
.combine(chuan1, chuan2)
.then(E)
.enqueue()
唯一鏈
什么是唯一鏈愧沟,就是同一時間內(nèi)隊列里不能存在相同名稱的任務(wù)蔬咬。
val request = OneTimeWorkRequestBuilder<MyWorker>().build()
WorkManager.getInstance().beginUniqueWork("tag",ExistingWorkPolicy.REPLACE,request,request,request)
從上面代碼我們可以看到沐寺,首先與之前不同的是林艘,這次我們用的是 beginUniqueWork 方法,這個方法的最后一個參數(shù)是一個可變長度的數(shù)組混坞,那就證明這一定是一根鏈條狐援。然后我們看這個方法的第一個參數(shù),要求輸入一個名稱究孕,這個名稱就是用來標(biāo)識任務(wù)的唯一性啥酱。那如果兩個不同的任務(wù)我們給了相同的名稱也是可以的,但是這兩個任務(wù)在隊列中只能存活一個厨诸。最后我們再來看第二個參數(shù) ExistingWorkPolicy,點(diǎn)進(jìn)去果然又雙叒是枚舉:
public enum ExistingWorkPolicy {
REPLACE,
KEEP,
APPEND
}
REPLACE:如果隊列里面已經(jīng)存在相同名稱的任務(wù)镶殷,并且該任務(wù)處于掛起狀態(tài)則替換之。
KEEP:如果隊列里面已經(jīng)存在相同名稱的任務(wù)微酬,并且該任務(wù)處于掛起狀態(tài)绘趋,則什么也不做颤陶。
APPEND:如果隊列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài)陷遮,則會緩存新任務(wù)滓走。當(dāng)隊列中所有任務(wù)執(zhí)行完畢后,以這個新任務(wù)做為序列的第一個任務(wù)帽馋。
總結(jié)
看到這里相信大家對于 WorkManager 的基本用法已經(jīng)了解的差不多了吧搅方!
另外通過這次對 WorkManager 的學(xué)習(xí),我們也看到官方在代碼里面也仍舊在用一些他自己不推薦使用的東西茬斧,比如 HashMap腰懂、HashSet、Enum 等项秉,只許州官放火不許百姓點(diǎn)燈绣溜?這很谷歌!其實(shí)不是的娄蔼,所謂萬事無絕對怖喻,只要你夠自信,自己做好取舍岁诉,掌握平衡锚沸,用什么還是由你自己做主!