WorkManager

本篇文章完全轉(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 就是一種隱式廣播,因為它將發(fā)送到注冊的所有偵聽器,讓后者知道設(shè)備上的某些軟件包已被替換。 不過,ACTION_MY_PACKAGE_REPLACED 不是隱式廣播,因為不管已為該廣播注冊偵聽器的其他應(yīng)用有多少,它都會只發(fā)送到軟件包已被替換的應(yīng)用。 應(yīng)用可以繼續(xù)在它們的清單中注冊顯式廣播。 應(yīng)用可以在運(yùn)行時使用 Context.registerReceiver() 為任意廣播(不管是隱式還是顯式)注冊接收器。 需要簽名權(quá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 實現(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 類,并實現(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不支持序列化傳值!這一點讓我怎么說啊,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)//注意看這里?。。?        .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ù),因為任務(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 到底都包涵什么,我們點進(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 枚舉其實就是用來給我們做最后的結(jié)果判斷的。但是要注意其中有個已掛起 BLOCKED,這是啥子情況?通過看它的注釋,我們得知,如果 WorkRequest 的約束沒有通過,那么這個任務(wù)就會處于掛起狀態(tài)。

接下來,Data 當(dāng)然就是我們在任務(wù)中 doWork 的返回值了。看到這里,我感覺谷歌大佬的設(shè)計思維還是非常之強(qiáng)的,把狀態(tài)和返回值同時輸出,非常方便我們做判斷的同時來取值,并且這樣的設(shè)計就可以達(dá)到‘多次返回’的效果,有時間一定要去看一下源碼,先立個 flag!

任務(wù)鏈

在很多場景中,我們需要把不同的任務(wù)弄成一個隊列,比如在用戶注冊的時候,我們要先驗證手機(jī)短信驗證碼,驗證成功后再注冊,注冊成功后再調(diào)登陸接口實現(xiàn)自動登陸。類似這樣相似的邏輯比比皆是,實話說筆者以前都是在 service 里面用 rxjava 來實現(xiàn)的。但是現(xiàn)在 service 在 Android8.0版本以上系統(tǒng)不能用了怎么辦?當(dāng)然還是用我們今天學(xué)到的 WorkManager 來實現(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,點進(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 等,只許州官放火不許百姓點燈?這很谷歌!其實不是的,所謂萬事無絕對,只要你夠自信,自己做好取舍,掌握平衡,用什么還是由你自己做主!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 13,900評論 2 59
  • 1、概述 在 I / O '18中,Google發(fā)布了Android Jetpack。它是一組庫,工具和架構(gòu)指南,...
    高丕基閱讀 7,673評論 1 12
  • 01 小狗錢錢引發(fā)的思考 群主大人分享小狗錢錢音頻,然后群里就熱鬧了起來,有的說“女兒學(xué)書法,鼓勵她教其他同學(xué),在...
    陳念媛閱讀 204評論 0 0
  • 一部豆瓣評分9.0的電影,又是徐崢主演的,沒有看影評,直接沖著口碑去的。如此高的口碑和呼聲之下,究竟給我的感覺是震...
    檸綰綰閱讀 374評論 0 3

友情鏈接更多精彩內(nèi)容