Android Jetpack - 使用 WorkManager 管理后臺任務

作為 Android Jetpack 中的新組件,WorkManager 負責用來管理后臺任務,它和一個異步任務以及 Service 有什么區(qū)別呢?看完你就知道了。

相關類

我們先來看看 WorkManager 相關的幾個類:

  • Worker
    任務的執(zhí)行者,是一個抽象類,需要繼承它實現(xiàn)要執(zhí)行的任務。

  • WorkRequest
    指定讓哪個 Woker 執(zhí)行任務,指定執(zhí)行的環(huán)境,執(zhí)行的順序等。
    要使用它的子類 OneTimeWorkRequest 或 PeriodicWorkRequest。

  • WorkManager
    管理任務請求和任務隊列,發(fā)起的 WorkRequest 會進入它的任務隊列。

  • WorkStatus
    包含有任務的狀態(tài)和任務的信息,以 LiveData 的形式提供給觀察者。

接下來是 WorkManager 的簡單使用。

使用

WorkManager 的實現(xiàn)包括以下幾個步驟。

依賴

在 build.gradle 添加如下依賴:

implementation "android.arch.work:work-runtime:$work_version"
implementation "android.arch.work:work-firebase:$work_version"

定義 Worker

我們定義 MainWorker 繼承 Worker,發(fā)現(xiàn)需要重寫 doWork 方法,并且需要返回任務的狀態(tài) WorkerResult:

class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        // 要執(zhí)行的任務
        return WorkerResult.SUCCESS
    }
}

我們暫時什么都不做,直接返回任務執(zhí)行完成 WorkerResult.SUCCESS。

定義 WorkRequest

在 MainActivity 中定義 WorkRequest:

val request = OneTimeWorkRequest.Builder(MainWorker::class.java).build()

OneTimeWorkRequest 意味著這個任務只需執(zhí)行一遍。

加入任務隊列

要讓任務執(zhí)行,需要將 WorkRequest 加入任務隊列:

WorkManager.getInstance().enqueue(request)

現(xiàn)在加入任務隊列后,任務會馬上得到執(zhí)行。但需要注意的是,這句代碼的作用是將任務加入任務隊列,而不是執(zhí)行任務,至于區(qū)別后面會講到。

數(shù)據(jù)交互

后臺任務少不了數(shù)據(jù)的交互,我們看一下數(shù)據(jù)是如何傳入傳出的。

input

先是在 Activity 傳數(shù)據(jù)給 Worker ,我們傳一個格式化過的時間過去:

val dateFormat = SimpleDateFormat("hh:mm:ss", Locale.getDefault())

val data = Data.Builder()
        .putString("time", dateFormat.format(Date()))
        .build()

val request = OneTimeWorkRequest.Builder(DemoWorker::class.java)
        .setInputData(data)
        .build()

使用 WorkRequest 的 setInputData 方法傳遞 Data,Data 的使用和 Bundle 差不多。

在 Worker 中,從 inputData 可以取到數(shù)據(jù),這里取到后簡單打印一下:

class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        Log.d("WorkManager", inputData.getString("time",""))
        return WorkerResult.SUCCESS
    }
}

output

當任務處理完了,需要將處理結果返回。傳入的是 inputData,傳出就是 outputData:

class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        Log.d("MainWorker", inputData.getString("time",""))
        outputData = Data.Builder()
            .putString("name", "SouthernBox")
            .build()
        return WorkerResult.SUCCESS
    }
}

每一個 WorkRequest 都會有一個 id,通過 id 可以獲取到對應任務的 WorkStatus,并且是以 LiveData 形式提供的:

WorkManager.getInstance()
        .getStatusById(request.id)
        .observe(this, Observer { workStatus ->
            if (workStatus != null && workStatus.state.isFinished) {
                Log.d("MainActivity", workStatus.outputData.getString("name", ""))
            }
        })

如果需要取消一個在隊列中的任務,也是通過 id 實現(xiàn)的:

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

這樣我們就完成了一個最簡單的 WorkManager,執(zhí)行一下就可以看到打印的結果了。

特性

到目前為止都是基本操作,和一個普通的異步任務沒有太大區(qū)別,接下來我們看看它不一樣的一些地方。

環(huán)境約束

WorkManager 允許我們指定任務執(zhí)行的環(huán)境,比如網(wǎng)絡已連接、電量充足時等,在滿足條件的情況下任務才會執(zhí)行。

可指定的條件及設置方法如下:

val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)  // 網(wǎng)絡狀態(tài)
        .setRequiresBatteryNotLow(true)                 // 不在電量不足時執(zhí)行
        .setRequiresCharging(true)                      // 在充電時執(zhí)行
        .setRequiresStorageNotLow(true)                 // 不在存儲容量不足時執(zhí)行
        .setRequiresDeviceIdle(true)                    // 在待機狀態(tài)下執(zhí)行,需要 API 23
        .build()

val request = OneTimeWorkRequest.Builder(MainWorker::class.java)
        .setConstraints(constraints)
        .build()

這個很好理解,除了網(wǎng)絡狀態(tài),其他設置項都是傳入一個布爾值,網(wǎng)絡狀態(tài)可選值如下:

狀態(tài) 說明
NOT_REQUIRED 沒有要求
CONNECTED 網(wǎng)絡連接
UNMETERED 連接無限流量的網(wǎng)絡
METERED 連接按流量計費的網(wǎng)絡
NOT_ROAMING 連接非漫游網(wǎng)絡

我們試一下效果,添加一個需要聯(lián)網(wǎng)的條件,Activity 代碼如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()

        val dateFormat = SimpleDateFormat("hh:mm:ss", Locale.getDefault())
        val data = Data.Builder()
                .putString("date", dateFormat.format(Date()))
                .build()

        val request = OneTimeWorkRequest
                .Builder(MainWorker::class.java)
                .setConstraints(constraints)
                .setInputData(data)
                .build()

        WorkManager.getInstance().enqueue(request)

        WorkManager.getInstance()
                .getStatusById(request.id)
                .observe(this, Observer<WorkStatus> { workStatus ->
                    if (workStatus != null && workStatus.state.isFinished) {
                        Log.d("MainActivity",
                                workStatus.outputData.getString("name", ""))
                    }
                })

    }
}

打開應用之前,先把網(wǎng)絡關閉,打開后發(fā)現(xiàn) Worker 并沒有打印時間,這時候再把網(wǎng)連上,就會看到打印出時間了。

這也是為什么前面說 WorkManager.getInstance().enqueue(request) 是將任務加入任務隊列,并不代表馬上執(zhí)行任務,因為任務可能需要等到滿足環(huán)境條件的情況才會執(zhí)行。

強大的生命力

還是一樣的代碼,我們來做點不一樣的操作:

  1. 斷網(wǎng)后運行
  2. 將進程殺掉
  3. 聯(lián)網(wǎng)
  4. 再次運行

不出意外的話,這時候你會看到有兩個時間的打印,而且兩個時間還不一樣,這是為什么呢?

第一個時間是第一次運行后,加入了任務隊列,但還沒有執(zhí)行的任務。第二個則是本次執(zhí)行的任務打印的。這說明了,就算進程被殺掉,任務還是存在,甚至如果重啟手機,任務依然會在滿足條件的情況下得到執(zhí)行。

這是 WorkManager 的另一個特點,一旦發(fā)起一個任務,任務是可以保證一定會被執(zhí)行的,就算退出應用,甚至重啟手機都阻止不了他。但可能由于添加了環(huán)境約束等原因,它執(zhí)行的時間是不確定的。

當應用正在運行時,它會在當前的進程中啟用一個子線程執(zhí)行。應用沒有運行的情況下啟用,它則會自己選擇一種合適的方式在后臺運行。具體是什么方式和 Android 的版本和依賴環(huán)境有關:

定時任務

前面說了 OneTimeWorkRequest 是指任務只需要執(zhí)行一遍,而 PeriodicWorkRequest 則可以發(fā)起一個多次執(zhí)行的定時任務:

val request = PeriodicWorkRequest
        .Builder(MainWorker::class.java, 15, TimeUnit.MINUTES)
        .setConstraints(constraints)
        .setInputData(data)
        .build()

這樣,發(fā)起的任務就會每隔 15 分鐘執(zhí)行一次。除了需要傳入間隔時間,使用起來跟 OneTimeWorkRequest 是沒有區(qū)別的。

你可能會想更頻繁的去執(zhí)行一個任務,比如幾秒鐘執(zhí)行一遍,但很遺憾,最小時間間隔就是 15 分鐘,看一下源碼就知道了。

還有需要注意的是,定時任務并不是說經(jīng)過指定時間后它就馬上執(zhí)行,而是經(jīng)過這一段時間后,等到滿足約束條件等情況時,它才執(zhí)行。

任務鏈

WorkManager 允許我們按照一定的順序執(zhí)行任務,比如我想 A、B、C 三個任務按先后順序執(zhí)行:

可以這樣寫,把它們組成一條任務鏈:

WorkManager.getInstance()
        .beginWith(workA)
        .then(workB)
        .then(workC)
        .enqueue()

這樣的話,上一個任務的 outputData 會成為下一個任務的 inputData。

再更復雜一點,我想 A 和 B 同時執(zhí)行,它們都執(zhí)行完之后,再執(zhí)行 C:

也是可以實現(xiàn)的:

WorkManager.getInstance()
        .beginWith(workA,workB)
        .then(workC)
        .enqueue()

再更更復雜一點,如果我想這樣:

這樣就需要先把 A、B 和 C、D 分別組成一條任務鏈,再進行聯(lián)結:

val chain1 = WorkManager.getInstance()
        .beginWith(workA)
        .then(workB)
val chain2 = WorkManager.getInstance()
        .beginWith(workC)
        .then(workD)
val chain3 = WorkContinuation
        .combine(chain1, chain2)
        .then(workE)
chain3.enqueue()

再更更更復雜一點,如果我把定時任務放進去會怎樣?不好意思,鏈式任務只支持 OneTimeWorkRequest。

使用任務鏈,我們可以將各種任務進行模塊化。同樣的,任務鏈不保證每個任務執(zhí)行的時間,但是保證它們執(zhí)行的先后順序。

任務唯一性

很多情況下,我們希望在任務隊列里,同一個任務只存在一個,避免任務的重復執(zhí)行,這時候可以用到 beginUniqueWork 這個方法:

WorkManager.getInstance()
        .beginUniqueWork("unique", ExistingWorkPolicy.REPLACE, request)
        .enqueue()

需要傳入一個任務的標簽,和重復任務的執(zhí)行方式,可取值如下:

狀態(tài) 說明
REPLACE 刪除已有的任務,添加現(xiàn)有的任務
KEEP 什么都不做,不添加新任務,讓已有的繼續(xù)執(zhí)行
APPEND 加入已有任務的任務鏈最末端

但這種方式也是只支持 OneTimeWorkRequest。如果是 PeriodicWorkRequest,我想到的辦法是每次執(zhí)行之前,根據(jù)標簽去取消已有的任務。

以上,就是本文對 WorkManager 的簡單介紹和用法講解。

?;?

這里引入一個思考,既然 WorkManager 的生命力這么強,還可以實現(xiàn)定時任務,那能不能讓我們的應用生命力也這么強?換句話說,能不能用它來?;?

要是上面有細看的話,你應該已經(jīng)發(fā)現(xiàn)這幾點了:

  • 定時任務有最小間隔時間的限制,是 15 分鐘
  • 只有程序運行時,任務才會得到執(zhí)行
  • 無法拉起 Activity

總之,用 WorkManager 保活是不可能了,這輩子都不可能?;盍?。

使用場景?

很明顯,WorkManager 區(qū)別于異步任務,它更像是一個 Service。基本上,WorkManager 能做的,Service 也能做,我并沒有想到有什么情況是非用 WorkManger 不可的。

但反觀 Service,泛濫的 Service 后臺任務可能是引起 Android 系統(tǒng)卡頓的主要原因,這幾年 Google 也對 Service 也做了一些限制。

對 Service 的限制

Android 6.0 (API 23)

休眠模式:在關閉手機屏幕后,系統(tǒng)會禁止應用的網(wǎng)絡請求等功能。

Android 8.0(API 26)

在某些不被允許的情況下,調(diào)用 startService 會拋異常。

但目前很多 APP 的 target API 還在 23 以下,因為不想處理運行時權限,更別說 API 26 了?;诖?,2017 年年底,谷歌采取了少有的強硬措施。

對 Target API 的要求

2018 年 8 月起

所有新開發(fā)的應用,Target API 必須是 26 或以上。

2018 年 11 月起

所有已發(fā)布的應用,Target API 必須更新到 26 或以上。

2019 年起

每次發(fā)布新版本后,所有應用都必須在一年內(nèi)將 Target API 更新到最新版本

雖然這些措施對國內(nèi)的環(huán)境沒有辦法造成直接影響,但這也會成為一種發(fā)展指導方向。

更合理的后臺任務管理

說了這么多,我想表達的是,在不久的將來,在某些情況下,Service 已經(jīng)沒卵用了!

而 WorkManager 作為一個更合理的后臺任務管理庫,在這種情況下就是一個更好的選擇了。

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

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

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,324評論 25 708
  • 那是一個很大很舒適青旅。公共空間里有來自世界各地的人。還有小吧臺,乒乓球臺,和一片很大的露臺,露臺的邊上有高腳凳,...
    微瀾Grazia閱讀 804評論 5 3
  • 一, “無情的人,最長情。” 這是沁心第一次跟安風表達自己愛意的時候,安風告訴她的話。沁心說她聽不懂,那個時候沁心...
    焦黑的兔子閱讀 749評論 3 42
  • 希望明天通過答辯 多忙一個星期的論文格式修改 把所有畢業(yè)表格填完 再加上一個月緊張刺激的面試培訓 希望一個月后 我...
    薯條船長閱讀 147評論 0 1

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