一. Background
首先這兩個概念是從Android L加入的轻专,最初的目的是為了省電!
從名字可以看出舌剂,是一個計劃安排,但肯定不是為了笔钜活就是了霍转,我們可以通過官方一張圖來看一下他是個什么原理。
對于后臺網(wǎng)絡(luò)加載一汽,最初是一進行網(wǎng)絡(luò)加載就喚醒設(shè)備避消,加載就喚醒設(shè)備,這個電量浪費可想而知召夹,當電量低的時候岩喷,進行喚醒更不合適,因此在Android L后面加了此API监憎,限制后臺喚醒纱意,系統(tǒng)會在合適的時候一起調(diào)度去運行,然后就形成了下面的圖形鲸阔。
這點應該跟MIUI的對齊喚醒類似偷霉。
所以根據(jù)上面所說,我們也可以提前知道后面的Api里面為什么會提供一些低電量停止任務(wù)褐筛,充電時執(zhí)行任務(wù)的方法类少。
But!
看起來這么高大上的技術(shù)死讹,國內(nèi)的廠商卻用他來做甭鞯危活!T蘧妓忍!
實際上,在Android5上愧旦,JobScheduler的運行效果相當顯著世剖,在某測試機上測試,基本上可以按照時間周期性調(diào)度笤虫。 (當然國內(nèi)各大廠的系統(tǒng)還是太強了……)
但隨著系統(tǒng)的迭代旁瘫,在Android后面的版本上祖凫,系統(tǒng)底層所做的限制越來越多,包括周期執(zhí)行時間不得低于15分鐘酬凳,這就讓IM類應用很難受了惠况,還是要去另尋他法。
但無論如何宁仔,背硗溃活都是一個偽命題,就算是加入了系統(tǒng)白名單也不一定能存活或者重新喚醒翎苫,國內(nèi)某鵝廠的Tim所采用的Linux底層喚醒权埠,也免不了被一殺啥消息都收不到。
所以我們不談?wù)摫煎谍;钊帘危嗟氖怯懻揓obScheduler和JobService用在執(zhí)行后臺簡單任務(wù)上面。
二. Use
JobScheduler是需要協(xié)同JobService來一起使用的呐粘,在準備實現(xiàn)一個Schedule之前满俗,我們要實現(xiàn)一個JobService。
1. 擼起袖子實現(xiàn)的 JobService
首先查看一下文檔, JobService的繼承關(guān)系如下作岖。
JobService 是一個abstract class漫雷,繼承自Service,那么他當然也會擁有一些Service擁有的特性(使用特點)鳍咱,比如說onCreate,onStartCommand与柑,注冊<service>等等谤辜。
對于其中的方法,主要是兩個abstract的method价捧,一個final的方法丑念,而其中的abstract就是我們需要自行去實現(xiàn)的。
擼起袖子就是一頓繼承结蟋。
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class DemoJobService extends JobService {
@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.d("tag","onStartJob");
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
Log.d("tag","onStopJob");
return false;
}
}
闊以看到脯倚,我們已經(jīng)實現(xiàn)了其中的方法,分別是對應job啟動的onStartJob()和job結(jié)束的onStopJob()方法嵌屎。
當然推正,不要忘記我們的API是在Android L里面加入的,所以加入@RequiresApi注解宝惰。
2. 記得注冊了喵~
剛才說過玩service一定不要忘了注冊植榕,所以在各個地方,Google都提醒了你 別忘記注冊D岫帷W鸩小炒瘸!
Android Studio里也有,但是本喵不會截warning的圖
<service android:name=".DemoJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"
/>
這家伙與眾不同的一點就是需要配置一個permission寝衫。
3. 關(guān)鍵人物 - Scheduler
對于JobScheduler顷扩,理論上是暴露給外人用的,比如我們在MainActivity中慰毅。
int jobId = 1;
JobScheduler jobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName jobService = new ComponentName(getPackageName(),
DemoJobService.class.getName());
JobInfo jobInfo = new JobInfo.Builder(jobId,jobService)
.setPeriodic(15 * 60 * 1000)
.setPersisted(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build();
if(jobScheduler != null){
jobScheduler.schedule(jobInfo);
}
但我們可愛的Java程序猿們總喜歡吧功能封裝起來,給他扣一頂可愛的static帽子隘截,然后公布于眾。
于是修改DemoJobService代碼事富,加一個靜態(tài)的invoke()方法技俐。
public static void invoke(){
int jobId = 1;
JobScheduler jobScheduler = (JobScheduler)
App.instance().getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName jobService = new ComponentName(App.instance().getPackageName(),
DemoJobService.class.getName());
JobInfo jobInfo = new JobInfo.Builder(jobId,jobService)
.setPeriodic(15 * 60 * 1000)
.setPersisted(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build();
if(jobScheduler != null){
jobScheduler.schedule(jobInfo);
}
}
核心是最后一句,用系統(tǒng)的JobScheduler统台,執(zhí)行了一個自定義的JobInfo雕擂,仔細查看schedule()方法。
這家伙還有返回值贱勃,代表了schedule的結(jié)果井赌。
安排?
ok, return RESULT_SUCCESS
這家伙甚至是一個abstract method,至于誰實現(xiàn)了他贵扰,我們先不用管仇穗。
這樣調(diào)用變的簡單起來。
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DemoJobService.invoke();
}
});
好了戚绕,你可以運行一下看看效果了纹坐,雖然你什么都不可能看到,偶爾有幸可以看到你的log,為什么這么說舞丛,我們后面為你揭曉耘子。
三. Introduce
上面我們已經(jīng)跑了一個只有一個 button的破界面了,可是更強大的機制在后臺運行著呢球切。
1. 基礎(chǔ)api
int jobId = 1;
JobScheduler jobScheduler = (JobScheduler)
App.instance().getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName jobService = new ComponentName(App.instance().getPackageName(),
DemoJobService.class.getName());
JobInfo jobInfo = new JobInfo.Builder(jobId,jobService)
.setPeriodic(15 * 60 * 1000)
.setPersisted(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build();
if(jobScheduler != null){
jobScheduler.schedule(jobInfo);
}
還是不變的代碼谷誓,變心了的文章。
你變了吨凑,變得開始講api怎么使用了捍歪,你再也不是balabala給我說的天花亂墜的,我一句聽不懂的鸵钝,覺得你超帥的糙臼,講系統(tǒng)源碼了的那個大佬了。
嫌棄我蒋伦?那我們簡單了解即可弓摘。(自導自演好歡樂)
- jobId就是job的身份證,身份證號有啥特點就不用我說了吧痕届。
- JobScheduler是從系統(tǒng)服務(wù)獲取出來的用于執(zhí)行job的大佬韧献,至于App.instance()是我自己實現(xiàn)的獲取context的方法末患,下面是代碼,記得添加Application 的name屬性(不會用自定義Application的請面Google思過)
public class App extends Application {
protected static App instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
}
public static Context instance() {
return instance.getApplicationContext();
}
}
JobInfo是基于構(gòu)造者模式來實現(xiàn)的锤窑,其Builder默認需要接受jobId和JobService所包裝的ComponentName璧针,構(gòu)造一個ComponentName對象需要兩個參數(shù),首先是包名渊啰,其次是要被安排上的JobService的名字探橱。
setPeriodic(xxx毫秒),重復執(zhí)行周期绘证,設(shè)置了這個屬性后隧膏,系統(tǒng)會每隔xxx毫秒來執(zhí)行一次,勤勤懇懇嚷那,不會說累胞枕,結(jié)合 setPersisted(true) 使用,媽媽再也不用擔心我關(guān)了機jobSevice就不重復執(zhí)行了餒魏宽。
注1. Android L的時候腐泻,時間可以設(shè)的很小,比如說10s執(zhí)行一次队询,系統(tǒng)也很認真的10s執(zhí)行一次派桩,但是隨著國內(nèi)各廠利用此Api進行保活手段越來越高明蚌斩,和Google越來越機靈铆惑,在后來的Android版本上,這個時間被限制到15分鐘以上了……尷尬送膳。
注2. setPersisted(true)鸭津,true為關(guān)機后再開機不影響任務(wù),false為關(guān)機后就死翹翹了肠缨。
- setRequiredNetworkType() ,需要在什么網(wǎng)絡(luò)下執(zhí)行盏阶,這里我們選的是NETWORK_TYPE_ANY晒奕,代表任何網(wǎng)絡(luò)均可。
2. 高級api
翻譯文檔走起名斟!
誰會干那么無聊的事情呢脑慧,況且我English也不是very well啊,所以這里就撿幾個重點的說一下吧砰盐,有些東西闷袒,可能你這輩子有永不到哦,(你非要用那你就用……)
setMinimumLatency (long minLatencyMillis)
設(shè)置job延遲一段時間后執(zhí)行:誰還不是個寶寶岩梳,就不能等我打扮完再安排我囊骤?ps:打扮時間是
minLatencyMillis 毫秒
調(diào)用了此方法后又設(shè)置了setPeriodic周期將會 boom晃择!
按照邏輯想想也不對是把,設(shè)置了周期又設(shè)置延遲也物,你讓系統(tǒng)怎么執(zhí)行宫屠,按照誰來走?左右為難啊滑蚯。
setOverrideDeadline (long maxExecutionDelayMillis)
設(shè)置此Job的最大延遲調(diào)度浪蹂,無論任何條件,即使有條件不滿足告材,Job也將在該截止時間前運行坤次,說白了就是一個系統(tǒng)執(zhí)行此job的deadline疆虚。
同樣涉馅,設(shè)置了周期時間會拋異常。
setRequiredNetworkType (int networkType)
在某種 網(wǎng)絡(luò)類型下才可以執(zhí)行此任務(wù)只嚣,比如說無網(wǎng)灿渴,任何網(wǎng)絡(luò)洛波,wifi,4g等等
setRequiresBatteryNotLow (boolean batteryNotLow)
設(shè)置是否低電量才執(zhí)行這個任務(wù)
setRequiresCharging (boolean requiresCharging)
設(shè)置是否在充電時執(zhí)行任務(wù)
setRequiresDeviceIdle (boolean requiresDeviceIdle)
設(shè)置是否在交互時執(zhí)行任務(wù)
當用戶玩手機的時候骚露,就不執(zhí)行蹬挤,不玩了就執(zhí)行。
setRequiresStorageNotLow (boolean storageNotLow)
指定此Job是否只在可用存儲空間太低時運行棘幸,默認為false焰扳。
講Api簡直是世界上最無聊的事情了。我們來點刺激的误续。
3. 起始
又回到我們的JobService吨悍。
對于開始,這個話題蹋嵌,在這里就從onStartJob說起吧育瓜,設(shè)置了schedule之后,系統(tǒng)進入調(diào)度job狀態(tài)栽烂,而這個調(diào)度跟手機電量躏仇,網(wǎng)絡(luò),balabala都有關(guān)腺办,也有可能因為國產(chǎn)手機的一鍵清理全部死翹翹焰手,但無論如何,我們都要說下去怀喉。
假設(shè)現(xiàn)在某job被調(diào)度了书妻,進入了onStartJob()方法。
@Override
public boolean onStartJob(JobParameters jobParameters) {
//我是一個小任務(wù)
//我一會就執(zhí)行完了
//到return時已完成任務(wù)
return false;
}
@Override
public boolean onStartJob(JobParameters jobParameters) {
//我是一個耗時任務(wù)
//半天才執(zhí)行完
//到return時還沒執(zhí)行完躬拢,需要手動結(jié)束
return true;
}
首先onStartJob和onStopJob都是在主線程執(zhí)行的躲履,而onStartJob有這樣兩種情況见间,非耗時任務(wù)和耗時任務(wù),對于這兩種任務(wù)崇呵,系統(tǒng)要求我們返回false和true兩種值缤剧。(對于自己的任務(wù)是否耗時開發(fā)者自己清楚,要是自欺欺人的話域慷!哼哼荒辕!ANR來一發(fā)。)
首先我們要明白一點犹褒,這也是Google官方所做的限制抵窒,當我們的onStartJob返回值是false的時候,說明任務(wù)應執(zhí)行完(在return之前)叠骑,那就沒必要再去執(zhí)行onStopJob()方法, 因為我們完全可以在onStartJob最后面寫執(zhí)行完的邏輯李皇,因此Google文檔上有這么一段話。
這意味著宙枷,如果你的onStartJob返回false掉房,你的onStopJob不會執(zhí)行,千萬不要在onStopJob里面做無用功了慰丛。
但是如果你要返回true的話卓囚,意味著你的任務(wù)是一個耗時操作。
按照Service是在主線程執(zhí)行的規(guī)定诅病,你需要在onStartJob里面寫一個異步操作哪亿,這里用AsynTask和Handler都行。
在任務(wù)執(zhí)行結(jié)束的時候需要手動調(diào)用相關(guān)方法來結(jié)束本次任務(wù)贤笆,否則將會造成阻塞蝇棉,讓系統(tǒng)誤以為你一直在運行,還沒結(jié)束芥永,導致后面的任務(wù)沒法執(zhí)行篡殷。
joinFinished就是需要我們手動執(zhí)行的方法,它有兩個參數(shù)埋涧,第一個是JobParameters贴唇,第二個是wantsReschedule
首先第一個參數(shù)是從onStartJob方法傳遞的,這就要求我們要記錄這個參數(shù)值飞袋,第二個參數(shù)是bool類型,表示是否想要被重新調(diào)度链患,填true的話就會被重新加入調(diào)度隊列巧鸭。
其實對于JobService需要知道的就是這三個比較重要的方法以及他們何時應該被主動調(diào)用和被動調(diào)用。
四. Summary
對于JobScheduler和JobService其實是作為一個后臺任務(wù)來使用的麻捻,如果作為后臺备偃裕活的招數(shù)來使用不僅達不到省電的目的還更耗電呀袱。
其次對于國內(nèi)大廠定制的Rom,JobService有些時候也不是工作很正常郑叠,目前還沒找到一個合適的工具來測試這個API夜赵,唯一的adb命令也沒啥可參考的,(可能是我太弱了……)乡革,請有知道的大佬熱心分享一下寇僧。