耗電優化(三):JobScheduler,WorkManager
// 在android8.0以後的電量提醒問題。(文章分成JobScheduler和WorkManager兩個大部分,互不影響,可以自行查閱)
問題描述
在Android8.0以後的安卓手機上,為了實現App在後臺的時候也能接收到伺服器端的實時訊息,使用了Service,然而在關閉App或進入後臺時,系統則會經常彈出一個無法清除掉的訊息:“有耗電高的應用在後臺”。
為了解決這個問題,我們查閱了Android開發者的官方文件,有關後臺任務(Background Task)的部分。由於這部分文件只有英文的版本,所以簡單說明一下:
它首先解釋了不同的情況要如何選擇最合適的解決方案。下面有一個表格簡單說明了這個問題:
策略的選擇
不知道上面這個表格看完,你有沒有完全理解呢?
因為這篇文件中很多晦澀的專業英語,所以我在理解上也有很多的不解。總體來說它介紹了幾種解決方案。其中似乎比較新的WorkManager還在測試階段,所以我打算後面再去嘗試,這裡首先試試JobScheduler。
在使用JobSchdeuler進行嘗試的時候我首先找到了一個部落格作為參考——JobScheduler API的使用詳細:
首先我先建立了一個MyJobService類,執行的工作內容是向檔案寫入內容。
(在JobService中我還建立了一個AsyncTask用來非同步寫入檔案)
/**
* 編譯環境:
Android Studio 3.1.4
Build #AI-173.4907809, built on July 24, 2018
JRE: 1.8.0_152-release-1024-b02 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
*/
package com.hongfei.intsig.backgrounddemo;
import android.annotation.SuppressLint;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android. util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
@SuppressLint("NewApi")
public class MyJobService extends JobService {
private static final String TAG = "MyJobService";
@Override
public boolean onStartJob(JobParameters jobParameters) {
if (isNetworkConnected()) {
new SimpleDownloadTask().execute(jobParameters);
return true;
} else {
}
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
// Log.i(TAG, "stop job,名字是: " + jobParameters.getJobId());
return true;
}
private boolean isNetworkConnected() {
ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = manager.getActiveNetworkInfo();
return (info != null && info.isConnected());
}
private class SimpleDownloadTask extends AsyncTask<JobParameters, Void, String> {
private JobParameters mJobParam;
@Override
protected String doInBackground(JobParameters... params) {
mJobParam = params[0];
// 具體的後臺操作
try{
File file = new File("/storage/emulated/0/1/testBackground.txt");
FileOutputStream fos = new FileOutputStream(file, true);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd--HH時mm分ss秒SSS");
String content = "測試內容,JobID為:" + mJobParam.getJobId() + "。在時間:" + formatter.format(new Date()) + "列印完成\n";
byte [] bytes = content.getBytes();
fos.write(bytes);
fos.close();
return "成功寫入";
}
catch(Exception e){
e.printStackTrace();
return "未成功! " + e.getMessage();
}
}
@Override
protected void onPostExecute(String result) {
jobFinished(mJobParam, true);
super.onPostExecute(result);
}
}
}
有了這個JobService之後,在MainActivity中,建立一個按鈕,執行以下函式(建立一個Scheduler)
/**
* 按鈕的回撥函式
*/
@SuppressLint("NewApi")
private void doBackgroundJob() {
int jobId;
// 建立三個id遞增的job Service
for (int i = 0; i < 3; i++) {
jobId = 0;
JobInfo jobInfo = new JobInfo.Builder(jobId, mJobService)
.setMinimumLatency(10000)// 設定任務執行最少延遲時間
.setOverrideDeadline(60000)// 設定deadline,若到期還沒有達到規定的條件則會開始執行
.setRequiresCharging(false)// 設定是否充電的條件
.setRequiresDeviceIdle(false)// 設定手機是否空閒的條件
.build();
mTvResult.append("scheduling job " + i + "。在時間:" + new Date().toString() + "!\n");
mJobScheduler.schedule(jobInfo);
}
}
點選按鈕後,就會發現過了一段時間之後會打印出一段文字。
這裡為了能夠更好地理解,我們將按鈕的回撥函式改為下面的函式:
/**
* 按鈕的回撥函式修改
*/
@SuppressLint("NewApi")
private void doBackgroundJob() {
int jobId = 201;
for (int i = 0; i < 3; i++) {
jobId = jobId + i;
JobInfo jobInfo = new JobInfo.Builder(jobId, mJobService)
.setPeriodic(MIN_PERIOD_MILLIS, MIN_FLEX_MILLIS)
.build();
mJobScheduler.schedule(jobInfo);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd--HH時mm分ss秒SSS");
try {
File file = new File("/storage/emulated/0/1/testBackground.txt");
FileOutputStream fos = new FileOutputStream(file, true);
String content = "開始計劃任務,JobID為:" + jobId+ "。在時間:" + formatter.format(new Date()) + "列印完成\n";
byte [] bytes = content.getBytes();
fos.write(bytes);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
mTvResult.append(formatter.format(new Date())+ "\n");
}
}
這樣就可以把大部分需要檢查的內容都列印到日誌檔案中了。點選按鈕之後,就把手機放在一邊,任憑他關閉螢幕,就算把APP關閉到後臺也無影響(但是不能強行清除後臺。)
好了,這個調研就先到這裡,讓手機跑一會兒之後我把日誌檔案貼上來~現在我們轉為去研究一下有關WorkManager的內容吧。
1天后:我又翻開了測試機的測試檔案,沒想到昨天上午開始的Job,後來一直沒怎麼使用,Job的內容就這麼奇怪,具體日誌檔案如下:
開始計劃任務,JobID為:201。在時間:2018/10/11--11時07分07秒823列印完成 開始計劃任務,JobID為:202。在時間:2018/10/11--11時07分07秒834列印完成 開始計劃任務,JobID為:204。在時間:2018/10/11--11時07分07秒846列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時17分22秒437列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時17分22秒445列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時17分22秒453列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時17分52秒493列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時17分57秒505列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時17分57秒513列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時18分52秒832列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時18分57秒566列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時19分02秒574列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時21分52秒477列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時21分52秒485列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時21分52秒490列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時25分52秒567列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時25分57秒573列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時25分57秒582列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時33分52秒631列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時33分57秒632列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時34分02秒725列印完成 測試內容,JobID為:202。在時間:2018/10/11--11時50分14秒637列印完成 測試內容,JobID為:201。在時間:2018/10/11--11時50分14秒645列印完成 測試內容,JobID為:204。在時間:2018/10/11--11時50分14秒654列印完成 測試內容,JobID為:204。在時間:2018/10/11--12時22分32秒740列印完成 測試內容,JobID為:201。在時間:2018/10/11--12時22分32秒752列印完成 測試內容,JobID為:202。在時間:2018/10/11--12時22分32秒778列印完成 測試內容,JobID為:202。在時間:2018/10/11--18時01分27秒071列印完成 測試內容,JobID為:204。在時間:2018/10/11--18時01分27秒095列印完成 測試內容,JobID為:201。在時間:2018/10/11--18時01分27秒109列印完成 測試內容,JobID為:202。在時間:2018/10/11--22時48分53秒938列印完成 測試內容,JobID為:204。在時間:2018/10/11--22時48分53秒979列印完成 測試內容,JobID為:201。在時間:2018/10/11--22時48分53秒985列印完成
這時候使用命令列
adb shell'dumpsys jobscheduler | grep 包名'
的指令也終於發現沒有後臺Service了。
唯一能夠確定的是使用JobScheduler的話,只要把APP清除了,就一定會停止任務。
WorkManager
從WorkManager的官方文件入手。
這一頁大概只是說了:WorkMangaer是一個完全智慧的任務分配器。你可以完全把要執行的任務丟給他,他就會幫你選擇最優的辦法(但是要是那種關閉程式就可以結束的任務就別扔給他啦,可以扔給執行緒池[ThreadPool])。
這後面有關WorkManager的部分還有兩頁,一頁基礎功能,一頁進階功能。
考慮到我們要實現的只是迴圈工作,我猜第一頁-WorkManager basics的最後一部分-RecurringTasks就可以實現。總之我們先繼續往下看吧~
文章第一段就告訴我們,如果想要把WorkManager庫匯入你的專案,需要參考另外一篇文章-Adding Components to your Project.。
為了一會我們的專案能夠正常使用WorkManager,這裡我們就花些時間先把匯入工作做好:
- 首先確保我們專案的compileSdk在28或以上(開啟app級別的build.gradle檔案)。
- 確保compileSdk在28以上之後,就可以在dependencies內新增下面兩行語句
dependencies{
...
// 新增下面兩行程式碼
def work_version = "1.0.0-alpha10"
implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin
...
}
類和概念
簡單介紹了幾個WorkManager的重要類:
類名 | 說明 | 是否是抽象類 |
---|---|---|
Worker | 表明了要執行的任務。 | √ |
WorkRequest | 指明需要哪一個Worker去執行task 有兩個子類OneTimeWrokRequest和PeriodicWorkRequest,顧名思義。而且都有各自的Build()方法 | √ |
WorkManager | 為你的WorkRequest們排序,管理 | – |
WorkStatus | 觀測的狀態 | – |
典型的工作流程
這種流程官網舉了一個例子,類似於照片APP要不斷的壓縮處理儲存的圖片,這就是一個典型流程。重點是——你分配了這個任務以後就可以忘了它了,不需要關心什麼時間真正的執行。
// todo:這裡是單次執行的任務,和我們的需求不符合,之後再看。
任務限定條件
這一部分講任務執行的條件的設定。
// todo:這裡和我們的需求不符合,之後再看。
給任務加TAG
就是一個集中處理,可以理解成微信好友的分組orz
重複性任務
其實這裡和前面的典型的工作流程差不多,不過由於前面沒有講述,所以這裡具體描述一下過程。
- 首先建立一個繼承自Worker的類:
public class MyFileWorker extends Worker {
// 自定義你自己的Worker
}
- Worker是一個抽線類,所以我們首先要實現必須要實現的方法:
public class MyFileWorker extends Worker {
public MyFileWorker(
@NonNull Context context,
@NonNull WorkerParameters params) {
super(context, params);
}
@Override
public Worker.Result doWork() {
//todo: 新增你自己的任務
return Result.SUCCESS;
// 返回Result.SUCCESS表示任務成功執行。
// 返回Result.FAILUER表示任務執行失敗但是不用重試。
// 返回Result.RETRY表示任務執行失敗稍後重試。
}
}
- 這裡貼一下我的Worker的最終效果:
import android.content.Context;
import android.support.annotation.NonNull;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class MyFileWorker extends Worker {
public MyFileWorker(
@NonNull Context context,
@NonNull WorkerParameters params) {
super(context, params);
}
@Override
public Worker.Result doWork() {
boolean isWriteFileSuccessful = testWriteIntoFile();
if(isWriteFileSuccessful)
return Result.SUCCESS;
else
return Result.RETRY;
// (Returning RETRY tells WorkManager to try this task again
// later; FAILURE says not to try again.)
}
/**
* 一個向檔案內寫入的函式,成功返回true,失敗返回false
*/
private boolean testWriteIntoFile() {
try{
File file = new File("/storage/emulated/0/1/testBackground.txt");
FileOutputStream fos = new FileOutputStream(file, true); // 向檔案內追加寫入
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd--HH時mm分ss秒SSS");
String content = "WorkManager的測試內容。在時間:" + formatter.format(new Date()) + "列印完成\n";
byte [] bytes = content.getBytes();
fos.write(bytes);
fos.close();
return true;
}
catch(Exception e){
e.printStackTrace();
return true;
}
}
}
就是個簡單的向檔案中寫日誌的功能。
- 在主介面新增一個按鈕,按鈕的回撥是週期性呼叫Work。具體程式碼如下:
private void initView() {
...
findViewById(R.id.btWorkManager).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
doBackgroundJobWithWorkManager();
}
});
}
private void doBackgroundJobWithWorkManager() {
PeriodicWorkRequest.Builder fileWritingBuilder =
new PeriodicWorkRequest.Builder(MyFileWorker.class, 15,
TimeUnit.MINUTES); // 這裡要注意15min是限制的最短時間了,具體可以檢視Builder函式的註釋。
// Create the actual work object:
PeriodicWorkRequest fileWritingWork = fileWritingBuilder.build();
// Then enqueue the recurring task:
WorkManager.getInstance().enqueue(fileWritingWork);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd--HH時mm分ss秒SSS");
try {
File file = new File("/storage/emulated/0/1/testBackground.txt");
FileOutputStream fos = new FileOutputStream(file, true);
String content = "開始WorkManager。在時間" + formatter.format(new Date()) + "列印完成\n";
byte [] bytes = content.getBytes();
fos.write(bytes);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
mTvResult.append("這是WorkManager" + formatter.format(new Date())+ "\n");
}
準備完成,開始除錯————我在10月12日15:40:45 122毫秒的時間點選了按鈕開始測試WorkManager。不出意外的話十五分鐘左右之後檔案內會追加第一行資訊,如果成功我會清空後臺,看看之後會不會出現。
(要指明的是,在我點選了開始任務的時間之後大概一秒鐘之內,在15:40:45 239秒就寫入了第一行測試內容。現在檔案大小是2.98kb)
15:56:55 335毫秒,列印了第二次日誌。
16:10:45 278毫秒,列印了第三次日誌。
現在我們關閉後臺程式。
16:27 檔案還是沒有變化,估計是出了問題或者乾脆不可行。
16:34 在檢視其他文件的同時將APP開啟,順便檢測一下是否會重新開始執行。
結果16:34:07 324毫秒就列印了第四次日誌。
16:48 再次關閉APP
16:52 還未打印出日誌,開啟APP
16:52:16 498馬上打印出了第五次日誌。
16:59,對APP做了少許修改,然後點選執行(不知道這樣會不會終止任務?)。
※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
17:07:46 692毫秒,打印出第六次日誌(但是要注意的是這裡我對MyFileWorker類做了一些修改,讓它打印出的內容是包含ID的,結果打印出了新的內容【我並沒有點選新建WorkManager的按鈕,也就是說它還在執行舊的Work,只是執行的時候到程式的程式碼處去找工作內容的時候,獲取到了新的工作內容。】)
※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
↑↑↑↑↑↑↑這個發現也很重要,總之是證明了正常情況下重啟APP還是會執行舊的WorkSequence↑↑↑↑↑
這裡附上日誌檔案WorkManager部分的全部內容:
開始WorkManager。在時間2018/10/12--15時40分45秒120列印完成
WorkManager的測試內容。在時間:2018/10/12--15時40分45秒239列印完成
WorkManager的測試內容。在時間:2018/10/12--15時56分55秒335列印完成
WorkManager的測試內容。在時間:2018/10/12--16時10分45秒278列印完成
WorkManager的測試內容。在時間:2018/10/12--16時34分07秒234列印完成
WorkManager的測試內容。在時間:2018/10/12--16時52分16秒498列印完成
WorkManager,序列7949ca4a-b92e-4ffc-b8ab-db7eea3724b0的測試內容。在時間:2018/10/12--17時07分46秒692列印完成
參考簡書部落格-Android Jetpack - 使用 WorkManager 管理後臺任務的**“保活?”**部分,原文內容是:
這裡引入一個思考,既然 WorkManager 的生命力這麼強,還可以實現定時任務,那能不能讓我們的應用生命力也這麼強?換句話說,能不能用它來保活?
要是上面有細看的話,你應該已經發現這幾點了:
- 定時任務有最小間隔時間的限制,是 15 分鐘
- 只有程式執行時,任務才會得到執行
- 無法拉起 Activity
總之,用 WorkManager 保活是不可能了,這輩子都不可能保活了。
其中第二點——“只有程式執行時,任務才會得到執行。”和我們觀測到的情況相一致。
也就是說應該是很難做到即使關閉APP還是能夠一直重新整理的情況了……但是可以保證程式在後臺也可以執行。並且關閉APP再啟動的時候也不用重新發布任務。
總結:
- JobScheduler和WorkManager都只能在APP存活的時候執行,但是定時器是一直工作的。
- 關閉APP再啟動,JobScheduler並不能夠直接繼續執行,但是WorkManager可以。
- 如果重啟APP的時候,WorkManager任務的計時器應該已經執行了一次或多次,則會立即開始執行。
- 重啟App之後WorkManager如果直接執行了一個任務,則從這個時候開始算新的週期,不會按舊有周期走。
參考: (JobScheduler部分)
https://www.jianshu.com/p/1f2103d3d2a2
https://blog.csdn.net/allisonchen/article/details/79218713
https://developer.android.com/reference/android/app/job/JobScheduler
參考:(WorkManager部分)
https://www.jianshu.com/p/e495ee6e84de
作者:任冉rr
連結:https://www.jianshu.com/p/b12a1163c4c2