1. JobSchedule在功耗上的影响
1.1 Doze模式
Android 6.0(API 级别 23)引入了低电耗模式,当用户设备未插接电源、处于静止状态且屏幕关闭时,该模式会推迟 CPU 和网络活动,从而延长电池寿命。而 Android 7.0 则通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持式设备装在口袋里)时应用部分 CPU 和网络限制,进一步增强了低电耗模式。
![](https://i-blog.csdnimg.cn/blog_migrate/3846c9bce8075bad8d6940ecdd354f40.png)
当设备处于充电状态且屏幕已关闭一定时间后,设备会进入低电耗模式并应用第一部分限制:关闭应用网络访问、推迟作业和同步。
如果进入低电耗模式后设备处于静止状态达到一定时间,系统则会对PowerManager.WakeLock、AlarmManager闹铃、GPS 和 WLAN 扫描等限制。
无论是应用部分还是全部低电耗模式限制,系统都会唤醒设备以提供简短的维护时间窗口,在此窗口期间,应用程序可以访问网络并执行任何被推迟的作业/同步。
![](https://i-blog.csdnimg.cn/blog_migrate/6fda339c03b82a170052902117be4b57.png)
1.2 静态广播对Doze模式的影响
目前,移动设备经历频繁的连接变更,例如在 WLAN 和移动数据之间切换时,可以通过在应用清单中注册一个接收器来侦听隐式 CONNECTIVITY_ACTION 广播,让应用能够监控这些变更。由于很多应用会注册接收此广播,因此单次网络切换即会导致所有应用被唤醒并同时处理此广播。
再或者在应用开发过程中常有开发者试图监听TimeTick广播以做一些周期性的工作,这个广播是是1分钟一次,每次TimeTick广播到来的时候设备都会被唤醒处理这个周期工作。
设备被唤醒处理该广播事件后会延长一段时间再进入Doze模式,方便短时间内再次处理即将到来的广播事件,网络连接等消息。如果频繁接收到该静态广播的唤醒,将导致移动设备无法进入Doze模式。例如用户午休将手机放置桌上,可能在这段放置时间内由于频繁接收到广播设备而不能进入Doze模式,减少电池使用寿命。
1.3 JobSchedule在Doze模式下的工作行为
JobSchedule的宗旨就是把一些不是特别紧急的任务放到更合适的时机批量处理。这样做有两个好处:
- 避免频繁的唤醒硬件模块,造成不必要的电量消耗;
- 避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量。
针对第一点:我们通过一个在蜂窝网络下的网络请求例子来说明(蜂窝网络下的请求相对是比较耗电的)。
Android系统为了尽可能的增加设备的续航,会不断的关闭各种硬件模块来节省电量。当我们的App在设备处于休眠状态下想要执行一次网络请求的时候;首先需要唤醒设备,接着会发送数据请求,然后等待服务端返回的结果,最后再经过一段时间的等待才会慢慢进入休眠状态。
Google在他们的在线课程中展示过一张图移动网络请求的图:
![图 3 移动网络请求图](https://i-blog.csdnimg.cn/blog_migrate/e0f6f955000551b50ae4883addec6c73.png)
通过上面这张电量消耗图我们看到,在唤醒设备、发送数据以及接受数据的瞬间都会造成大量的电量消耗。同时系统为了你的下一次网络请求不用再次唤醒设备,会等待一段时间再让设备进入休眠。如果你的请求是间歇性的,那么这些等待休眠的时间内造成的电量消耗其实也是多余的。
JobSchedule API的优化方案就是将这些间歇性的网络请求任务推迟到某个时间点(针对时间点下面会有介绍)来集中处理。
![图 4 移动网络请求处理图](https://i-blog.csdnimg.cn/blog_migrate/2919e1c6dc4f54d1c5780768f4417be4.png)
通过上图我们可以看到,间歇性的网络请求被集中处理了;避免了重复的唤醒设备,同时也减少了设备等待休眠的次数,以此达到省电的目的。
针对第二点:我们可以通过JobSchedule API来优化请求时机,比如说用户的设备剩余电量已经不多了,那么对于一些及时性要求不高的任务我们就可以放到电量充足或者是设备充电阶段再执行;又比如说现在用户设备处于蜂窝网络状况或者弱网络环境下,那么我们就可以将这些任务放到WiFi网络或者网络情况良好的时侯再处理。前面在谈第一点的时候提到的“时间点”正是指的根据设备当前状况选择合适的时机。
总上, 使用JobSchedule避免了静态Broadcast的间歇性唤醒设备,同时也减少了设备等待休眠的次数,以此达到省电的目的。此外 JobSchedule可以选择合适时机执行Job, 比如用户设备电量极低或网络状况不好的情况,这样可以延长电池寿命,十分适合实时性要求不是很高的任务,比如云同步服务的拉活。
1.4 Doze模式对后台服务的影响
经过测试: Doze第一阶段和第二阶段均不会对service的后台线程造成影响,后台service启动的线程可以正常工作,但是进入Doze第二阶段后系统会强制释放不在前台的非系统应用的wakelock,所以如果你的应用service中依赖某个wakelock的部分也是不能工作的。(系统有豁免白名单,如果有需要请提醒用户在设置中将应用加入白名单)
官网说明: 在该时间窗结束后,应用将被视为处于 空闲 状态。 此时,系统将停止应用的后台服务,就像应用已经调用服务的“Service.stopSelf()”方法。
对于常驻后台service, 需要理解一下官网这个后台service
2. JobSchedule API简介
在Android开发中,会存在这么些场景 : 你需要在稍后的某个时间点或者当满足某个特定的条件时执行一个任务,例如当设备接通电源适配器或者连接到WIFI或者定时执行。幸运的是在API 21中,google提供了一个新叫做JobScheduler API的组件来处理这样的场景。
当一系列预置的条件被满足时,JobScheduler API为你的应用执行一个操作。与AlarmManager不同的是这个执行时间是不确定的。除此之外,JobScheduler API允许同时执行多个任务。这允许你的应用执行某些指定的任务时不需要考虑时机控制引起的电池消耗。
2.1 JobSchedule的基本使用
2.1.1 继承JobService实现自己的JobService,在onStartJob中实现自己定时触发逻辑
public class MyJobService extends JobService {
private static final int JOB_ID = 1001;
private static final String TAG = MyJobService.class.getSimpleName();
@Override
public boolean onStartJob(JobParameters params) {
Log.e(TAG, "MyJobService onStartJob " + this.toString());
String serviceName = "com.gome.jobscheduledemo.TargetService";
if (!isServiceRunning(getApplicationContext(), serviceName)) {
Intent intent1 = new Intent();
intent1.setClassName("com.gome.jobscheduledemo", serviceName);
getApplicationContext().startService(intent1);
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.e(TAG, "MyJobService onStopJob");
return false;
}
public static void scheduleService(Context context) {
}
private static boolean isServiceRunning(Context context, String s) {
return false;
}
}
当任务开始时会回调onStartJob(JobParameters params)方法。返回值是false时,系统认为该方法返回时任务已经执行完毕;返回值是true,系统认为这个任务正要被执行。当任务执行完毕时必须调用jobFinished(JobParameters params, boolean needsRescheduled)来通知系统。
当系统接收到一个取消请求时,系统会调用onStopJob(JobParameters params)方法取消正在等待执行的任务。
2.1.2 将service注册到AndroidManifest.xml
<service
android:name="com.gome.jobscheduledemo.MyJobService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
2.1.3 创建一个JobScheduler对象使用
使用JobInfo.Builder设置Job的参数
使用JobScheduler.schedule(builder.build()) 将job添加到系统队列中
public static void scheduleService(Context context) {
JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, new ComponentName(context.getPackageName(), MyJobService.class.getName()));
builder.setPersisted(true); //设置开机启动
builder.setPeriodic(JobInfo.getMinPeriodMillis()); //设置15分钟执行一次
js.cancel(JOB_ID);
js.schedule(builder.build());
}
2.1.4 将MyJobService添加进JobSchedule队列
在application的onCreate中或其他希望schedule任务的地方调用scheduelService(Context context),可以开启将MyJobService添加进JobSchedule队列。
MyJobService.scheduleService(this.getApplicationContext());
startService(new Intent(this.getApplicationContext(), MyJobService.class));
JobScheduler tm = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
List<JobInfo> infos = tm.getAllPendingJobs();
Log.d("MyJobService", "JobInfos : [" + infos.toString() + "]");
使用JobSchedule.getAllPending.Jobs()可以获取到所有在排队的Jobs。
2.2 JobSchedule设置的注意事项
- setPeriodic(long intervalMillis): 设置Job触发间隔时间,从API25开始setPeriodic设置的最小值是900000ms(即最小触发间隔为15分钟,这个间隔时间可能会导致手机不能进入Doze的第二阶段)
- setMinimumLatency(long minLatencyMillis): 设置Job的延迟执行时间,与setPeriodic(long time)方法不兼容,同时调用了就会引起异常;
- setOverrideDeadline(long maxExecutionDelayMillis): 设置任务最晚的延迟时间。如果到了规定的时间时其他条件还未满足,Job也会被启动。与setPeriodic(long time),同时调用会引发异常。
- setPersisted(boolean isPersisted): 当设备重启之后该Job是否还要继续执行。设置true时app需要申请RECEIVE_BOOT_COMPLETED权限。
- setRequiredNetworkType(int networkType): Job只有在满足指定的网络条件时才会被执行。默认条件是JobInfo.NETWORK_TYPE_NONE,即不管是否有网络Job都会被执行。另外两个可选类型,一种是JobInfo.NETWORK_TYPE_ANY,表明需要任意一种网络才使得Job可以执行。另一种是JobInfo.NETWORK_TYPE_UNMETERED,表示设备不是蜂窝网络( 比如在WIFI连接时 )时Job才会被执行。
- setRequiresCharging(boolean requiresCharging): 只有当设备在充电时Job才会被执行。
- setRequiresDeviceIdle(boolean requiresDeviceIdle): 只有当用户没有在使用该设备且有一段时间没有使用时才会启动该Job。
- MyJobService在AndroidManifest.xml中需要携带BIND_JOB_SERVICE权限。
2.3 JobSchedule 工作原理
如果想在将来达到一定条件下执行某项任务时,可以在一个实现了 JobService 的子类的onStartJob 方法中执行这项任务,使用 JobInfo 的 Builder 方法来设定条件并与实现了 JobService 的MyJobService的组件名绑定,然后调用系统服务 JobScheduler 的 schedule 方法。这样,即便在执行任务之前应用程序进程被杀,任务仍然会被执行,因为系统服务 JobSchedulerService 会使用 bindServiceAsUser 的方法把实现了 JobService 的MyJobService启动起来,并执行它的 onStartJob 方法。
JobSchedulerServidce在 SystemServer.java 中的 startOtherServices 启动,运行在system_server进程中。在 JobSchedulerServidce 创建时会初始化七个 Controller :ConnectivityController、TimeController、IdleController、BatteryController、AppIdleController、ContentObserverController和DeviceIdleJobsController。这些Controller会注册广播、定时器和ContentObserver等,当这些条件触发时,Controller会回调JobSchedulerServidce的onControllerStateChanged方法,然后会尝试执行executeRunnableJob(JobStatus job),最终会 bindServiceAsUser 我们注册的JobService,然后回调JobService的onStartJob方法。基本的工作流程如下图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8b5ca2255c8f314bf25f74b082ccf5f9.png)
3. Service 保活方案
Android Service保活主要在两个层面
- 提高service优先级,降低service被杀死的概率。
- 在service被杀死后进行拉活。。
3.1 灰色保活方案简介
startForeground(ID, new Notification()),可以将Service变成前台服务,所在进程就算退到后台,优先级只会降到PERCEPTIBLE_APP_ADJ(2)或者VISIBLE_APP_ADJ(1),一般不会被杀掉,Android的有个漏洞,如果两个Service通过同样的ID设置为前台进程,而其中一个通过stopForeground取消了前台显示,结果仍会保留一个前台服务,但不会在状态栏显示notification。优先级提高后,AMS只会杀死oom_adj大于SERVICE_ADJ(5)的进程,所以不会杀死目标service所在进程。(用户通过Settings-app forceStop应用仍然可以杀死进程。)
但是在android7.1的代码中Google已修复这个漏洞,所以在android7.1的代码版本上这个保活手段会失效,在通知栏会显示通知。修复代码如下:
741 private void cancelForegroudNotificationLocked(ServiceRecord r) {
742 if (r.foregroundId != 0) {
743 // First check to see if this app has any other active foreground services
744 // with the same notification ID. If so, we shouldn't actually cancel it,
745 // because that would wipe away the notification that still needs to be shown
746 // due the other service.
747 ServiceMap sm = getServiceMap(r.userId);
748 if (sm != null) {
749 for (int i = sm.mServicesByName.size()-1; i >= 0; i--) {
750 ServiceRecord other = sm.mServicesByName.valueAt(i);
751 if (other != r && other.foregroundId == r.foregroundId
752 && other.packageName.equals(r.packageName)) {
753 // Found one! Abort the cancel.
754 return;
755 }
756 }
757 }
758 r.cancelNotification();
759 }
760 }
如果在ServiceMap中发现仍然有相同id且packageName相同的service时直接返回,不会cancel notification。
3.2 Service自动重启START_STICKY
在运行onStartCommand后service进程被kill后,将保留在开始状态,但是不保留那些传入的intent。不久后service就会再次尝试重新创建,因为保留在开始状态,在创建 service后将保证调用onstartCommand。
Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起该Service。
进程被取得 Root 权限的管理工具或系统工具通过 forestop 或情况缓存与数据而停止掉无法重启。
3.3 基于JobSchedule机制的定时尝试拉活
JobSchedule可以满足某个特定的条件时执行一个任务,例如当设备接通电源适配器或者连接到WIFI或者定时执行。并且JobScheduleService运行在SystemServer进程中,即使app进程被杀死,只要JobService被注册到JobSchedule,到触发时间时系统就可以onBind app 中的 JobService 并回调 onStartJob 。
通过在onStartJob中查询目标Service是否在运行监测目标Service运行状态,如果目标Service死亡则重启目标Service。
3.4 Service自动重启(START_STICKY) + JobSchedule拉活保活方案验证
这套方案具有被杀死概率低,双重拉活service保证拉活稳定,功耗表现比广播好和可以兼容android 8.0等优点。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/1d84a00ee46012890955a303b7fb8c5f.png)
3.5 双Service守护
启动两个Service在不同的进程中,然后相互Bind,并且在ServiceConnection的OnService中重新拉起死掉的service达到守护的效果。此外双Service还有一个额外的有点是:它可以提高service的oom_adj,使得当前的service不容易被杀死。
- 如下是应用启动后的进程分别为com.dong.myapplication(7448)和com.dong.myapplication:remote(7467)
dong@dong-System-Product-Name:~$ adb shell ps|grep myapplication
u0_a79 7448 437 1919424 76828 SyS_epoll_ 0000000000 S com.dong.myapplication
u0_a79 7467 437 1710420 46356 SyS_epoll_ 0000000000 S com.dong.myapplication:remote
- 如下是两个Service启动后相互bind成功的log
dong@dong-System-Product-Name:~$ adb logcat |grep ServiceGuard
12-02 11:12:04.188 7448 7448 D ServiceGuard -->MyService: MyService2 connected
12-02 11:12:04.188 7467 7467 D ServiceGuard -->MyService2: MyService connected
- 如下我们杀掉目标MyService(在com.dong.myapplication进程中)后再打印进程,可以看到com.dong.myapplication(7576)进程号已经改变说明service已经被重启,进程守护效果达到。
dong@dong-System-Product-Name:~$ adb shell kill -9 7448
dong@dong-System-Product-Name:~$ adb shell ps|grep myapplication
u0_a79 7467 437 1710420 46768 SyS_epoll_ 783c708614 S com.dong.myapplication:remote
u0_a79 7576 437 1911568 75812 SyS_epoll_ 783c708614 S com.dong.myapplication
dong@dong-System-Product-Name:~$ adb logcat |grep ServiceGuard
12-02 11:12:04.188 7448 7448 D ServiceGuard -->MyService: MyService2 connected
12-02 11:12:04.188 7467 7467 D ServiceGuard -->MyService2: MyService connected
12-02 11:17:29.324 7467 7467 D ServiceGuard -->MyService2: MyService disconnected
12-02 11:17:30.121 7576 7576 D ServiceGuard -->MyService: MyService2 connected
12-02 11:17:30.121 7467 7467 D ServiceGuard -->MyService2: MyService connected
代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dong.myapplication">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyService"
android:enabled="true"
android:exported="true" />
<service android:process=":remote"
android:name=".MyService2"
android:enabled="true"
android:exported="true"></service>
</application>
</manifest>
//MyService.java
package com.dong.myapplication;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class MyService extends Service {
private static final String TAG = "ServiceGuard -->MyService";
private ServiceConnection sc = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "MyService2 connected");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "MyService2 disconnected");
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.dong.myapplication", "com.dong" +
".myapplication.MyService2"));
bindService(intent, sc, BIND_AUTO_CREATE);
}
};
public MyService() {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
bindService2();
return super.onStartCommand(intent, flags, startId);
}
private void bindService2() {
Intent intent2 = new Intent();
intent2.setComponent(new ComponentName("com.dong.myapplication", "com.dong" +
".myapplication.MyService2"));
bindService(intent2, sc, BIND_AUTO_CREATE);
}
@Override
public IBinder onBind(Intent intent) {
bindService2();
return new Binder();
}
}
//MyService2
package com.dong.myapplication;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class MyService2 extends Service {
private static final String TAG = "ServiceGuard -->MyService2";
private ServiceConnection sc = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "MyService connected");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "MyService disconnected");
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.dong.myapplication", "com.dong" +
".myapplication.MyService"));
bindService(intent, sc, BIND_AUTO_CREATE);
}
};
public MyService2() {
}
private void bindService2() {
Intent intent2 = new Intent();
intent2.setComponent(new ComponentName("com.dong.myapplication", "com.dong" +
".myapplication.MyService"));
bindService(intent2, sc, BIND_AUTO_CREATE);
}
@Override
public IBinder onBind(Intent intent) {
bindService2();
return new Binder();
}
}