本篇文章完全轉(zhuǎn)載于微笑的江豚 的博客地址:
https://my.oschina.net/JiangTun
如有問題,請及時(shí)聯(lián)系!
前言
以前我們在處理后臺(tái)任務(wù)時(shí),都是使Service(含IntentService)或線程、線程池。而Service不受頁面生命周期影響,可以常駐后臺(tái),所以很適合做一些定時(shí)、延時(shí)任務(wù),或者其他肉眼看不到的神秘勾當(dāng)。在處理一些復(fù)雜需求時(shí),比如監(jiān)聽網(wǎng)絡(luò)環(huán)境自動(dòng)暫停重啟后臺(tái)上傳下載這類任務(wù)時(shí),我們用Service結(jié)合Broadcast一起來做,非常麻煩,再加上傳輸進(jìn)度的回調(diào),更是讓人抓狂。
大量的后臺(tái)任務(wù)過度的消耗了設(shè)備的電量,比如多種第三方推送的Service都常駐后臺(tái),不良APP后臺(tái)自動(dòng)上傳用戶隱私帶來了隱私安全問題。
谷歌專項(xiàng)整頓
- 6.0 (API 級(jí) 23) 引入了 Doze 機(jī)制和應(yīng)用程序待機(jī)。當(dāng)屏幕關(guān)閉且設(shè)備靜止時(shí), 打盹模式會(huì)限制應(yīng)用程序的行為。應(yīng)用程序待機(jī)將未使用的應(yīng)用程序置于限制其網(wǎng)絡(luò)訪問、作業(yè)和同步的特殊狀態(tài)。
- Android 7.0 (API 級(jí) 24) 有限的隱性廣播和 Doze-on-the-go.
- Android 8.0 (API 級(jí) 26) 進(jìn)一步限制了后臺(tái)行為, 例如在后臺(tái)獲取位置并釋放緩存的 wakelocks。
尤其在Android O(8.0)中,谷歌對于后臺(tái)的限制幾乎可以稱之為變態(tài):
- Android 8.0 有一項(xiàng)復(fù)雜功能,系統(tǒng)不允許后臺(tái)應(yīng)用創(chuàng)建后臺(tái)服務(wù)。 因此,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService(),以在前臺(tái)啟動(dòng)新服務(wù)。 在系統(tǒng)創(chuàng)建服務(wù)后,應(yīng)用有五秒的時(shí)間來調(diào)用該服務(wù)的 startForeground() 方法以顯示新服務(wù)的用戶可見通知。 如果應(yīng)用在此時(shí)間限制內(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)用有多少,它都會(huì)只發(fā)送到軟件包已被替換的應(yīng)用。 應(yīng)用可以繼續(xù)在它們的清單中注冊顯式廣播。 應(yīng)用可以在運(yùn)行時(shí)使用 Context.registerReceiver() 為任意廣播(不管是隱式還是顯式)注冊接收器。 需要簽名權(quán)限的廣播不受此限制所限,因?yàn)檫@些廣播只會(huì)發(fā)送到使用相同證書簽名的應(yīng)用,而不是發(fā)送到設(shè)備上的所有應(yīng)用。 在許多情況下,之前注冊隱式廣播的應(yīng)用使用 JobScheduler 作業(yè)可以獲得類似的功能。
于此同時(shí),官方推薦用5.0推出的 JobScheduler 替換 Service + Broadcast 的方案。并且在 Android O,后臺(tái) Service 啟動(dòng)后的5秒內(nèi),如果不轉(zhuǎn)為前臺(tái) Service 就會(huì) 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 是一個(gè) Android 庫, 它在工作的觸發(fā)器 (如適當(dāng)?shù)木W(wǎng)絡(luò)狀態(tài)和電池條件) 滿足時(shí), 優(yōu)雅地運(yùn)行可推遲的后臺(tái)工作。WorkManager 盡可能使用框架 JobScheduler , 以幫助優(yōu)化電池壽命和批處理作業(yè)。在 Android 6.0 (API 級(jí) 23) 下面的設(shè)備上, 如果 WorkManager 已經(jīng)包含了應(yīng)用程序的依賴項(xiàng), 則嘗試使用 Firebase JobDispatcher 。否則, WorkManager 返回到自定義 AlarmManager 實(shí)現(xiàn), 以優(yōu)雅地處理您的后臺(tái)工作。
也就是說,WorkManager 可以自動(dòng)維護(hù)后臺(tái)任務(wù),同時(shí)可適應(yīng)不同的條件,同時(shí)滿足后臺(tái)Service 和靜態(tài)廣播,內(nèi)部維護(hù)著 JobScheduler,而在6.0以下系統(tǒng)版本則可自動(dò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 是一個(gè)抽象類,用來指定需要執(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é)束時(shí)會(huì)回調(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)然除了上述代碼中的方法之外,我們也可以重寫父級(jí)的getExtras(),并在此方法中把參數(shù)寫死再返回也是可以的。
這里WorkManager就有一個(gè)不是很人性的地方了,那就是WorkManager不支持序列化傳值!這一點(diǎn)讓我怎么說啊,intent和Bundle都支持序列化傳值,為什么偏偏這貨就不行?那么如果傳一個(gè)復(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 要求最后返回一個(gè) Result,這個(gè) Result 是一個(gè)枚舉,它有幾個(gè)固定的值:
- FAILURE 任務(wù)失敗。
- RETRY 遇到暫時(shí)性失敗,此時(shí)可使用WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit)來重試。
- SUCCESS 任務(wù)成功。
看到這里我就很奇怪,官方不推薦我們使用枚舉,但是自己卻一直在用,什么意思?
WorkRequest
也是一個(gè)抽象類,可以對 Work 進(jìn)行包裝,同時(shí)裝裱上一系列的約束(Constraints),這些 Constraints 用來向系統(tǒng)指明什么條件下,或者什么時(shí)候開始執(zhí)行任務(wù)。
WorkManager 向我們提供了 WorkRequest 的兩個(gè)子類:
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ù)時(shí)電池電量不能偏低。
public boolean requiresCharging ():在設(shè)備充電時(shí)才能執(zhí)行任務(wù)。
public boolean requiresDeviceIdle ():設(shè)備空閑時(shí)才能執(zhí)行。
public boolean requiresStorageNotLow ():設(shè)備儲(chǔ)存空間足夠時(shí)才能執(zhí)行。
addContentUriTrigger
@RequiresApi(24)
public @NonNull Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants)
指定是否在(Uri 指定的)內(nèi)容更新時(shí)執(zhí)行本次任務(wù)(只能用于 Api24及以上版本)。瞄了一眼源碼發(fā)現(xiàn)了一個(gè) 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;
}
特么驚呆了,居然是個(gè)HashSet,而HashSet的核心是個(gè)HashMap啊,谷歌聲明不建議用HashMap,當(dāng)然也就不建議用HashSet,可是官方自己在背地里面干的這些勾當(dāng)啊...
setRequiredNetworkType
public void setRequiredNetworkType (NetworkType requiredNetworkType)
指定任務(wù)執(zhí)行時(shí)的網(wǎng)絡(luò)狀態(tài)。其中狀態(tài)見下表:
枚舉 | 狀態(tài) |
---|---|
NOT_REQUIRED | 不需要網(wǎng)絡(luò) |
CONNECTED | 任何可用網(wǎng)絡(luò) |
UNMETERED | 需要不計(jì)量網(wǎng)絡(luò),如 WiFi |
NOT_ROAMING | 需要非漫游網(wǎng)絡(luò) |
METERED | 需要計(jì)量網(wǎng)絡(luò),如4G |
setRequiresBatteryNotLow
public void setRequiresBatteryNotLow (boolean requiresBatteryNotLow)
指定設(shè)備電池電量低于閥值時(shí)是否啟動(dòng)任務(wù),默認(rèn) false。
setRequiresCharging
public void setRequiresCharging (boolean requiresCharging)
指定設(shè)備在充電時(shí)是否啟動(dòng)任務(wù)。
setRequiresDeviceIdle
public void setRequiresDeviceIdle (boolean requiresDeviceIdle)
指明設(shè)備是否為空閑時(shí)是否啟動(dòng)任務(wù)
setRequiresStorageNotLow
public void setRequiresStorageNotLow (boolean requiresStorageNotLow)
指明設(shè)備儲(chǔ)存空間低于閥值時(shí)是否啟動(dòng)任務(wù)。給任務(wù)加約束:
val myConstraints = Constraints.Builder()
.setRequiresDeviceIdle(true)//指定{@link WorkRequest}運(yùn)行時(shí)設(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í)設(shè)備是否為空閑
.setRequiresStorageNotLow(true)//指定設(shè)備可用存儲(chǔ)是否不應(yīng)低于臨界閾值
.addContentUriTrigger(myUri,false)//指定內(nèi)容{@link android.net.Uri}時(shí)是否應(yīng)該運(yùn)行{@link WorkRequest}更新
.build()
val request = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS)
.setConstraints(myConstraints)//注意看這里!!!
.build()
給任務(wù)加標(biāo)簽分組
val request1 = OneTimeWorkRequestBuilder<MyWorker>()
.addTag("A")//標(biāo)簽
.build()
val request2 = OneTimeWorkRequestBuilder<MyWorker>()
.addTag("A")//標(biāo)簽
.build()
上述代碼我給兩個(gè)相同任務(wù)的request都加上了標(biāo)簽,使他們成為了一個(gè)組:A組。這樣的好處是以后可以直接控制整個(gè)組就行了,組內(nèi)的每個(gè)成員都會(huì)受到影響。
WorkManager
經(jīng)過上面的操作,相信我們已經(jīng)能夠成功創(chuàng)建 request 了,接下來我們就需要把任務(wù)放進(jìn)任務(wù)隊(duì)列,我們使用 WorkManager。
WorkManager 是個(gè)單例,它負(fù)責(zé)調(diào)度任務(wù)并且監(jiān)聽任務(wù)狀態(tài)。
WorkManager.getInstance().enqueue(request)
當(dāng)我們的 request 入列后,WorkManager 會(huì)給它分配一個(gè) work ID,之后我們可以使用這個(gè)work id 來取消或者停止任務(wù):
WorkManager.getInstance().cancelWorkById(request.id)
注意:WorkManager 并不一定能結(jié)束任務(wù),因?yàn)槿蝿?wù)有可能已經(jīng)執(zhí)行完畢了。
同時(shí),WorkManager 還提供了其他結(jié)束任務(wù)的方法:
- cancelAllWork():取消所有任務(wù)。
- cancelAllWorkByTag(tag:String):取消一組帶有相同標(biāo)簽的任務(wù)。
- cancelUniqueWork(uniqueWorkName:String):取消唯一任務(wù)。
WorkStatus
當(dāng) WorkManager 把任務(wù)加入隊(duì)列后,會(huì)為每個(gè)WorkRequest對象提供一個(gè) LiveData(如果這個(gè)東東不了解的話趕緊去學(xué))。 LiveData 持有 WorkStatus;通過觀察該 LiveData, 我們可以確定任務(wù)的當(dāng)前狀態(tài), 并在任務(wù)完成后獲取所有返回的值。
val liveData: LiveData<WorkStatus> = WorkManager.getInstance().getStatusById(request.id)
我們來看這個(gè) 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 這兩個(gè)屬性,首先看 State:
public enum State {
ENQUEUED,//已加入隊(duì)列
RUNNING,//運(yùn)行中
SUCCEEDED,//已成功
FAILED,//已失敗
BLOCKED,//已刮起
CANCELLED;//已取消
public boolean isFinished() { return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
}
}
這特么又一個(gè)枚舉。看過代碼之后,State 枚舉其實(shí)就是用來給我們做最后的結(jié)果判斷的。但是要注意其中有個(gè)已掛起 BLOCKED,這是啥子情況?通過看它的注釋,我們得知,如果 WorkRequest 的約束沒有通過,那么這個(gè)任務(wù)就會(huì)處于掛起狀態(tài)。
接下來,Data 當(dāng)然就是我們在任務(wù)中 doWork 的返回值了。看到這里,我感覺谷歌大佬的設(shè)計(jì)思維還是非常之強(qiáng)的,把狀態(tài)和返回值同時(shí)輸出,非常方便我們做判斷的同時(shí)來取值,并且這樣的設(shè)計(jì)就可以達(dá)到‘多次返回’的效果,有時(shí)間一定要去看一下源碼,先立個(gè) flag!
任務(wù)鏈
在很多場景中,我們需要把不同的任務(wù)弄成一個(gè)隊(duì)列,比如在用戶注冊的時(shí)候,我們要先驗(yàn)證手機(jī)短信驗(yàn)證碼,驗(yàn)證成功后再注冊,注冊成功后再調(diào)登陸接口實(shí)現(xiàn)自動(dòng)登陸。類似這樣相似的邏輯比比皆是,實(shí)話說筆者以前都是在 service 里面用 rxjava 來實(shí)現(xiàn)的。但是現(xiàn)在 service 在 Android8.0版本以上系統(tǒng)不能用了怎么辦?當(dāng)然還是用我們今天學(xué)到的 WorkManager 來實(shí)現(xiàn),接下來我們就一起看一下 WorkManager 的任務(wù)鏈。
- 鏈?zhǔn)絾?dòng)-并發(fā)
val request1 = OneTimeWorkRequestBuilder<MyWorker1>().build()
val request2 = OneTimeWorkRequestBuilder<MyWorker2>().build()
val request3 = OneTimeWorkRequestBuilder<MyWorker3>().build()
WorkManager.getInstance().beginWith(request1,request2,request3)
.enqueue()
這樣等同于 WorkManager 把一個(gè)個(gè)的 WorkRequest enqueue 進(jìn)隊(duì)列,但是這樣寫明顯更整齊!同時(shí)隊(duì)列中的任務(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,這期間如果有任何一個(gè)任務(wù)失敗(返回 Worker.WorkerResult.FAILURE),則整個(gè)隊(duì)列就會(huì)被中斷。
在任務(wù)鏈的串行中,也就是兩個(gè)任務(wù)使用了 then 操作符連接,那么上一個(gè)任務(wù)的返回值就會(huì)自動(dòng)轉(zhuǎn)為下一個(gè)任務(wù)的參數(shù)!
- combine 操作符-組合
現(xiàn)在我們有個(gè)復(fù)雜的需求:共有A、B、C、D、E這五個(gè)任務(wù),要求 AB 串行,CD 串行,但兩個(gè)串之間要并發(fā),并且最后要把兩個(gè)串的結(jié)果匯總到E。
我們看到這種復(fù)雜的業(yè)務(wù)邏輯,往往都會(huì)嚇一跳,但是牛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()
唯一鏈
什么是唯一鏈,就是同一時(shí)間內(nèi)隊(duì)列里不能存在相同名稱的任務(wù)。
val request = OneTimeWorkRequestBuilder<MyWorker>().build()
WorkManager.getInstance().beginUniqueWork("tag",ExistingWorkPolicy.REPLACE,request,request,request)
從上面代碼我們可以看到,首先與之前不同的是,這次我們用的是 beginUniqueWork 方法,這個(gè)方法的最后一個(gè)參數(shù)是一個(gè)可變長度的數(shù)組,那就證明這一定是一根鏈條。然后我們看這個(gè)方法的第一個(gè)參數(shù),要求輸入一個(gè)名稱,這個(gè)名稱就是用來標(biāo)識(shí)任務(wù)的唯一性。那如果兩個(gè)不同的任務(wù)我們給了相同的名稱也是可以的,但是這兩個(gè)任務(wù)在隊(duì)列中只能存活一個(gè)。最后我們再來看第二個(gè)參數(shù) ExistingWorkPolicy,點(diǎn)進(jìn)去果然又雙叒是枚舉:
public enum ExistingWorkPolicy {
REPLACE,
KEEP,
APPEND
}
REPLACE:如果隊(duì)列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài)則替換之。
KEEP:如果隊(duì)列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài),則什么也不做。
APPEND:如果隊(duì)列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài),則會(huì)緩存新任務(wù)。當(dāng)隊(duì)列中所有任務(wù)執(zhí)行完畢后,以這個(gè)新任務(wù)做為序列的第一個(gè)任務(wù)。
總結(jié)
看到這里相信大家對于 WorkManager 的基本用法已經(jīng)了解的差不多了吧!
另外通過這次對 WorkManager 的學(xué)習(xí),我們也看到官方在代碼里面也仍舊在用一些他自己不推薦使用的東西,比如 HashMap、HashSet、Enum 等,只許州官放火不許百姓點(diǎn)燈?這很谷歌!其實(shí)不是的,所謂萬事無絕對,只要你夠自信,自己做好取舍,掌握平衡,用什么還是由你自己做主!