1. 程式人生 > 其它 >耗電優化(三):JobScheduler,WorkManager

耗電優化(三):JobScheduler,WorkManager

技術標籤:androidjavaandroid安卓

// 在android8.0以後的電量提醒問題。(文章分成JobScheduler和WorkManager兩個大部分,互不影響,可以自行查閱)

問題描述

在Android8.0以後的安卓手機上,為了實現App在後臺的時候也能接收到伺服器端的實時訊息,使用了Service,然而在關閉App或進入後臺時,系統則會經常彈出一個無法清除掉的訊息:“有耗電高的應用在後臺”

為了解決這個問題,我們查閱了Android開發者的官方文件,有關後臺任務(Background Task)的部分。由於這部分文件只有英文的版本,所以簡單說明一下:

它首先解釋了不同的情況要如何選擇最合適的解決方案。下面有一個表格簡單說明了這個問題:

img

策略的選擇

不知道上面這個表格看完,你有沒有完全理解呢?
因為這篇文件中很多晦澀的專業英語,所以我在理解上也有很多的不解。總體來說它介紹了幾種解決方案。其中似乎比較新的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,這裡我們就花些時間先把匯入工作做好:

  1. 首先確保我們專案的compileSdk在28或以上(開啟app級別的build.gradle檔案)。
  2. 確保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

重複性任務

其實這裡和前面的典型的工作流程差不多,不過由於前面沒有講述,所以這裡具體描述一下過程。

  1. 首先建立一個繼承自Worker的類:
public class MyFileWorker extends Worker {
    // 自定義你自己的Worker
}
  1. 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表示任務執行失敗稍後重試。
    }
}
  1. 這裡貼一下我的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;
        }
    }
}

就是個簡單的向檔案中寫日誌的功能。

  1. 在主介面新增一個按鈕,按鈕的回撥是週期性呼叫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再啟動的時候也不用重新發布任務。


總結:

  1. JobScheduler和WorkManager都只能在APP存活的時候執行,但是定時器是一直工作的。
  2. 關閉APP再啟動,JobScheduler並不能夠直接繼續執行,但是WorkManager可以。
  3. 如果重啟APP的時候,WorkManager任務的計時器應該已經執行了一次或多次,則會立即開始執行。
  4. 重啟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