作為 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)不一樣的操作:
- 斷網(wǎng)后運(yùn)行
- 將進(jìn)程殺掉
- 聯(lián)網(wǎng)
- 再次運(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ù)管理庫蛤育,在這種情況下就是一個更好的選擇了宛官。