耗电优化(三):JobScheduler,WorkManager

// 在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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容