Android Jetpack - 使用 WorkManager 管理后臺任務(wù)

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

相關(guān)類

我們先來看看 WorkManager 相關(guān)的幾個類:

  • Worker
    任務(wù)的執(zhí)行者佛吓,是一個抽象類宵晚,需要繼承它實(shí)現(xiàn)要執(zhí)行的任務(wù)恨旱。

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

  • WorkManager
    管理任務(wù)請求和任務(wù)隊(duì)列钝凶,發(fā)起的 WorkRequest 會進(jìn)入它的任務(wù)隊(duì)列。

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

接下來是 WorkManager 的簡單使用。

使用

WorkManager 的實(shí)現(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 方法,并且需要返回任務(wù)的狀態(tài) WorkerResult:

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

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

定義 WorkRequest

在 MainActivity 中定義 WorkRequest:

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

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

加入任務(wù)隊(duì)列

要讓任務(wù)執(zhí)行孔祸,需要將 WorkRequest 加入任務(wù)隊(duì)列:

WorkManager.getInstance().enqueue(request)

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

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

后臺任務(wù)少不了數(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

當(dāng)任務(wù)處理完了,需要將處理結(jié)果返回掉盅。傳入的是 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 可以獲取到對應(yīng)任務(wù)的 WorkStatus趾痘,并且是以 LiveData 形式提供的:

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

如果需要取消一個在隊(duì)列中的任務(wù)慢哈,也是通過 id 實(shí)現(xiàn)的:

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

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

特性

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

環(huán)境約束

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

可指定的條件及設(shè)置方法如下:

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

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

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

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

我們試一下效果,添加一個需要聯(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", ""))
                    }
                })

    }
}

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

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

強(qiáng)大的生命力

還是一樣的代碼检疫,我們來做點(diǎn)不一樣的操作:

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

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

第一個時間是第一次運(yùn)行后,加入了任務(wù)隊(duì)列烛谊,但還沒有執(zhí)行的任務(wù)风响。第二個則是本次執(zhí)行的任務(wù)打印的。這說明了丹禀,就算進(jìn)程被殺掉状勤,任務(wù)還是存在,甚至如果重啟手機(jī)双泪,任務(wù)依然會在滿足條件的情況下得到執(zhí)行持搜。

這是 WorkManager 的另一個特點(diǎn),一旦發(fā)起一個任務(wù)焙矛,任務(wù)是可以保證一定會被執(zhí)行的葫盼,就算退出應(yīng)用,甚至重啟手機(jī)都阻止不了他村斟。但可能由于添加了環(huán)境約束等原因贫导,它執(zhí)行的時間是不確定的抛猫。

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

定時任務(wù)

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

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

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

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

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

任務(wù)鏈

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

可以這樣寫芙委,把它們組成一條任務(wù)鏈:

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

這樣的話,上一個任務(wù)的 outputData 會成為下一個任務(wù)的 inputData狂秦。

再更復(fù)雜一點(diǎn)灌侣,我想 A 和 B 同時執(zhí)行,它們都執(zhí)行完之后裂问,再執(zhí)行 C:

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

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

再更更復(fù)雜一點(diǎn)侧啼,如果我想這樣:

這樣就需要先把 A、B 和 C堪簿、D 分別組成一條任務(wù)鏈痊乾,再進(jìn)行聯(lián)結(jié):

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()

再更更更復(fù)雜一點(diǎn),如果我把定時任務(wù)放進(jìn)去會怎樣戴甩?不好意思符喝,鏈?zhǔn)饺蝿?wù)只支持 OneTimeWorkRequest。

使用任務(wù)鏈甜孤,我們可以將各種任務(wù)進(jìn)行模塊化协饲。同樣的畏腕,任務(wù)鏈不保證每個任務(wù)執(zhí)行的時間,但是保證它們執(zhí)行的先后順序茉稠。

任務(wù)唯一性

很多情況下描馅,我們希望在任務(wù)隊(duì)列里,同一個任務(wù)只存在一個而线,避免任務(wù)的重復(fù)執(zhí)行铭污,這時候可以用到 beginUniqueWork 這個方法:

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

需要傳入一個任務(wù)的標(biāo)簽,和重復(fù)任務(wù)的執(zhí)行方式膀篮,可取值如下:

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

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

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

绷浅保活?

這里引入一個思考,既然 WorkManager 的生命力這么強(qiáng)扼倘,還可以實(shí)現(xiàn)定時任務(wù)确封,那能不能讓我們的應(yīng)用生命力也這么強(qiáng)?換句話說再菊,能不能用它來庇绶剩活?

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

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

總之,用 WorkManager 甭逃铮活是不可能了秃症,這輩子都不可能保活了吕粹。

使用場景种柑?

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

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

對 Service 的限制

Android 6.0 (API 23)

休眠模式:在關(guān)閉手機(jī)屏幕后蚯姆,系統(tǒng)會禁止應(yīng)用的網(wǎng)絡(luò)請求等功能五续。

Android 8.0(API 26)

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

但目前很多 APP 的 target API 還在 23 以下疙驾,因?yàn)椴幌胩幚磉\(yùn)行時權(quán)限,更別說 API 26 了郭毕【S基于此,2017 年年底铣卡,谷歌采取了少有的強(qiáng)硬措施。

對 Target API 的要求

2018 年 8 月起

所有新開發(fā)的應(yīng)用偏竟,Target API 必須是 26 或以上煮落。

2018 年 11 月起

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

2019 年起

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

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

更合理的后臺任務(wù)管理

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

而 WorkManager 作為一個更合理的后臺任務(wù)管理庫蛤育,在這種情況下就是一個更好的選擇了宛官。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瓦糕,隨后出現(xiàn)的幾起案子底洗,更是在濱河造成了極大的恐慌,老刑警劉巖咕娄,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亥揖,死亡現(xiàn)場離奇詭異,居然都是意外死亡圣勒,警方通過查閱死者的電腦和手機(jī)费变,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門摧扇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胡控,你說我怎么就攤上這事扳剿。” “怎么了昼激?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵庇绽,是天一觀的道長。 經(jīng)常有香客問我橙困,道長瞧掺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任凡傅,我火速辦了婚禮辟狈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘夏跷。我一直安慰自己哼转,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布槽华。 她就那樣靜靜地躺著壹蔓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猫态。 梳的紋絲不亂的頭發(fā)上佣蓉,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音亲雪,去河邊找鬼勇凭。 笑死,一個胖子當(dāng)著我的面吹牛义辕,可吹牛的內(nèi)容都是我干的虾标。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼灌砖,長吁一口氣:“原來是場噩夢啊……” “哼夺巩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起周崭,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤柳譬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后续镇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體美澳,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了制跟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舅桩。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雨膨,靈堂內(nèi)的尸體忽然破棺而出擂涛,到底是詐尸還是另有隱情,我是刑警寧澤聊记,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布撒妈,位于F島的核電站,受9級特大地震影響排监,放射性物質(zhì)發(fā)生泄漏狰右。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一舆床、第九天 我趴在偏房一處隱蔽的房頂上張望棋蚌。 院中可真熱鬧,春花似錦挨队、人聲如沸谷暮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湿弦。三九已至,卻和暖如春情臭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赌蔑。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工俯在, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人娃惯。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓跷乐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親趾浅。 傳聞我的和親對象是個殘疾皇子愕提,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)皿哨,斷路器浅侨,智...
    卡卡羅2017閱讀 134,600評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,520評論 25 707
  • 那是一個很大很舒適青旅。公共空間里有來自世界各地的人证膨。還有小吧臺如输,乒乓球臺,和一片很大的露臺,露臺的邊上有高腳凳不见,...
    微瀾Grazia閱讀 692評論 5 3
  • 一澳化, “無情的人,最長情稳吮《泄龋” 這是沁心第一次跟安風(fēng)表達(dá)自己愛意的時候,安風(fēng)告訴她的話灶似。沁心說她聽不懂列林,那個時候沁心...
    焦黑的兔子閱讀 492評論 3 23
  • 希望明天通過答辯 多忙一個星期的論文格式修改 把所有畢業(yè)表格填完 再加上一個月緊張刺激的面試培訓(xùn) 希望一個月后 我...
    薯?xiàng)l船長閱讀 123評論 0 1