隨着Android版本的不斷更新,如何正確的處理後臺任務變得愈來愈複雜。所以, Google發佈了 WorkManager(做爲JetPack的一部分)來幫助開發者解決這一難題。android
在學習WorkManager以前,首先得知道咱們爲何須要它。本文將從如下三部分來闡述:git
Android系統的內核是基於Linux內核的,它與其餘那些基於Linux內核的系統的主要差異在於Android系統沒有交換空間(Swap space)。當系統內存資源已被耗盡,可是又有額外的內存資源請求的時候,內存中不活動的頁面會被移動到交換空間。交換空間是磁盤上的一塊區域,所以其訪問速度比物理內存慢。ubuntu
鑑於此,Android系統引入了OOM( Out Of Memory ) Killer 來解決內存資源被耗盡的問題。它的做用是根據進程所消耗的內存大小以及進程的「visibility state」來決定是否殺死這個進程,從而達到釋放內存的目的。bash
Activity Manager會給不一樣狀態下的進程設置相對應的oom_adj 值。下面是一些示例:服務器
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel. These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0 //前臺進程
setprop ro.VISIBLE_APP_ADJ 1 //可見進程
setprop ro.SECONDARY_SERVER_ADJ 2 //次要服務
setprop ro.BACKUP_APP_ADJ 2 //備份進程
setprop ro.HOME_APP_ADJ 4 //桌面進程
setprop ro.HIDDEN_APP_MIN_ADJ 7 //後臺進程
setprop ro.CONTENT_PROVIDER_ADJ 14 //內容供應節點
setprop ro.EMPTY_APP_ADJ 15 //空進程
複製代碼
進程的omm_adj 值越大,它被 OOM killer 殺死的可能性越大。OOM killer是依據系統空閒的內存空間大小和omm_adj閾值的組合規則來殺死進程的。好比,當空閒的內存空間大小小於X1時,殺死那些omm_adj值大於Y1的進程。它的基本處理流程以下圖所示:markdown
到如今爲止,我但願你知道兩點:網絡
A Service is an application component that can perform long-running operations in the background, and it does not provide a user interface.架構
使用service的理由以下:app
可是也存在一個缺點:我開發了本身的第一個應用,它居然在不到3個小時的時間內將電池電量從100%消耗至0%,由於個人應用開啓了一個service:每3分鐘從服務器中獲取數據。ide
那時候我還只是一個年輕的沒有經驗的開發者。但不知爲什麼,6年以後,仍然有許多未知的應用程序在作着一樣的事情。
每一位開發者能夠毫無限制地在後臺執行着任何他們想作的操做。google也意識到了這一點,並試圖採起一些改進的措施。
從 Marshmallow 開始,而後是 Nougat , Android系統引入了休眠模式 (Doze mode)
何爲休眠模式?簡而言之,當用戶關閉了手機屏幕以後,系統會自動進入休眠模式,禁止全部應用的網絡請求、數據同步、GPS、鬧鐘、wifi掃描等功能,直到用戶從新點亮屏幕或者手機接入了電源,這樣能夠有效節省手機的電量。
但這感受像是滄海一粟,所以從Android Oreo (API 26) 開始,Google 作了進一步改進:若是一個應用的目標版本爲Android 8.0,當它在某些不被容許建立後臺服務的場景下,調用了Service的startService()方法,該方法會拋出IllegalStateException。這個問題能夠經過調整targeting SDK的值來解決,一些知名應用的target API都是22,由於他們不肯意處理運行時權限。
可是,接下來你會發現:
說了這麼多 - (我相信你會得出一樣的結論):
咱們所熟知的Servcie已經被棄用了,由於它再也不被容許在後臺執行長時間的操做,而這倒是它最初被設計出來的目的。
除了Foreground service以外,咱們已經沒有任何理由再去使用Service了。
首先舉個簡單的例子:有一個簡單的網絡請求,它能下載幾千字節的數據。最簡單的方法(並不正確的方法)就是開啓一個單獨的線程來執行這一請求。
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
executor.submit(myWork);
複製代碼
再考慮下登陸場景。用戶填寫了郵箱、密碼,而後點擊了登陸按鈕。用戶的手機是3G網絡,信號不好,接着他走進了電梯。
當應用正在執行登陸網絡請求的時候,用戶接了一個電話。
OkHttp 默認的超時時間很長
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
複製代碼
一般咱們都會設置網絡請求重試的次數爲3
所以, 最壞的狀況是: 3 * 30 = 90 秒.
如今請回答一個問題 ——
當你的應用退到後臺以後,你就什麼都不知道了。正如咱們所瞭解的,你不能期望你的應用進程會一直存活,以完成網絡請求,處理響應並保存用戶登陸信息。更不用說用戶的手機還有可能進入離線模式,失去網絡鏈接。
從用戶的角度來看,我已經輸入了個人郵箱和密碼,而且點擊了登陸按鈕,所以我應該已經登陸成功了。假設沒有登陸成功的話,用戶會認爲你的應用的用戶體驗不好,但事實上這並非用戶體驗的問題,而是一個技術問題。
接下來你就會思考,ok,一旦個人應用即將退到後臺,我就開啓Service去執行登陸操做,可是你不能!!!
這時候JobScheduler
就能派上用場了。
ComponentName service = new ComponentName(this, MyJobService.class);
JobScheduler mJobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent)
.setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setExtras(extras).build();
mJobScheduler.schedule(jobInfo);
複製代碼
JobScheduler首先會調度一個任務,而後在合適的時機(好比說延遲若干時間以後,或者等手機空閒了)系統會開啓你的MyJobService,而後執行onStartJob()裏的處理邏輯。這個想法理論上很好,可是它只在API>21的系統上可用,並且在API 21&22的系統裏JobScheduler還存在一個重大bug。
這意味着你只能在API>22的系統上使用JobScheduler。
若是你應用的minSDK < 23,你可使用JobDispatcher
。
Job myJob = firebaseJobDispatcher.newJobBuilder()
.setService(SmartService.class)
.setTag(SmartService.LOCATION_SMART_JOB)
.setReplaceCurrent(false)
.setConstraints(ON_ANY_NETWORK)
.build();
firebaseJobDispatcher.mustSchedule(myJob);
複製代碼
等等, 它須要使用 Google Play Services!!
所以若是你打算使用JobDispatcher,你將會拋棄數千萬用戶。
所以,JobDispatcher 可能不是一個好的選擇。那麼AlarmManager呢?經過AlarmManager去輪詢檢查網絡請求是否執行成功,若是沒有的話嘗試再次執行它?
若是你仍是想用Service來當即執行網絡請求的話,能夠選擇JobIntentService
當SDK<26的時候,採用IntentService來執行任務;當SDK ≥ 26的時候,採用JobScheduler來執行任務。
Ahhhh… 它沒法在 Android Oreo 上當即執行請求。
因此回到咱們開始的地方:當應用退到後臺的時候,依據Android系統的版本和手機的狀態,選擇合適的任務調度器來調度執行後臺任務。
天吶,要作到既能節省手機電池的的電量又能爲用戶提供驚豔的用戶體驗實在是太難了吧!
依據手機所處的狀態、Android系統版本、手機是否擁有Google Play Services,能夠選擇對應的解決方案。你可能會嘗試着本身去實現這一整套複雜的處理邏輯。好消息是Android framework的設計者已經聽到了咱們的抱怨,他們決定去解決這個問題。
On the last Google I/O Android framework, the team announced WorkManager:
WorkManager aims to simplify the developer experience by providing a first-class API for system-driven background processing. It is intended for background jobs that should run even if the app is no longer in the foreground. Where possible, it uses JobScheduler or Firebase JobDispatcher to do the work; if your app is in the foreground, it will even try to do the work directly in your process.
哇! 這正是咱們須要的!
WorkManager庫包含如下幾個組件:
WorkManager
接收帶參數和約束條件的WorkRequest,並將其排入隊列。
Worker
你只須要實現doWork() 這一個方法,它是執行在一個單獨的後臺線程裏的。全部須要在後臺執行的任務都在這個方法裏完成。
WorkRequest
給Worker設置參數和約束條件(好比,是否聯網、是否接通電源)等。
WorkResult
Success, Failure, Retry.
Data
傳遞給Worker的持久化的鍵值對。
首先新建一個繼承了Worker的類,並實現它的 doWork()方法:
public class LocationUploadWorker extends Worker {
...
//Upload last passed location to the server
public WorkerResult doWork() {
ServerReport serverReport = new ServerReport(getInputData().getDouble(LOCATION_LONG, 0),
getInputData().getDouble(LOCATION_LAT, 0), getInputData().getLong(LOCATION_TIME,
0));
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef =
database.getReference("WorkerReport v" + android.os.Build.VERSION.SDK_INT);
myRef.push().setValue(serverReport);
return WorkerResult.SUCCESS;
}
}
複製代碼
而後使用WorkManager將它排入任務隊列:
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType
.CONNECTED).build();
Data inputData = new Data.Builder()
.putDouble(LocationUploadWorker.LOCATION_LAT, location.getLatitude())
.putDouble(LocationUploadWorker.LOCATION_LONG, location.getLongitude())
.putLong(LocationUploadWorker.LOCATION_TIME, location.getTime())
.build();
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(LocationUploadWorker.class)
.setConstraints(constraints).setInputData(inputData).build();
WorkManager.getInstance().enqueue(uploadWork);
複製代碼
接下來,WorkManager將會合理地調度執行你的任務;它會存儲任務全部的參數,任務的細節,更新任務的狀態。你甚至可使用LiveData來訂閱觀察它的狀態變化:
WorkManager.getInstance().getStatusById(locationWork.getId()).observe(this,
workStatus -> {
if(workStatus!=null && workStatus.getState().isFinished()){
...
}
});
複製代碼
WorkManager庫的架構圖以下所示:
它能作的還不止這些。
你能夠經過它來執行定時任務:
Constraints constraints = new Constraints.Builder().setRequiredNetworkType
(NetworkType.CONNECTED).build();
PeriodicWorkRequest locationWork = new PeriodicWorkRequest.Builder(LocationWork
.class, 15, TimeUnit.MINUTES).addTag(LocationWork.TAG)
.setConstraints(constraints).build();
WorkManager.getInstance().enqueue(locationWork);
複製代碼
你也可讓多個任務按順序執行:
WorkManager.getInstance(this)
.beginWith(Work.from(LocationWork.class))
.then(Work.from(LocationUploadWorker.class))
.enqueue();
複製代碼
你還可讓多個任務同時執行:
WorkManager.getInstance(this).enqueue(Work.from(LocationWork.class,
LocationUploadWorker.class));
複製代碼
固然你也能夠將以上三種任務執行方式結合起來使用。
注意: 你不能構建一個將定時任務和一次性任務混合在一塊兒的任務鏈。
WorkManager能夠作不少事情: 取消任務, 組合任務, 構建任務鏈, 將一個任務的參數合併到另外一個任務。我建議你去查閱官方文檔,裏面有許多好的例子。
爲了遵循節省用戶手機電池電量的原則,Android每個版本都在不斷改進,處理後臺任務變得十分複雜。感謝Android團隊,如今咱們可使用WorkManager來更加簡單直接地處理處理後臺任務。