Android O 后台限制解决方式

1、前言

Android O版本上面,Android做了一定的限制,目的就是加快Android的运行速度,导致在Android O版本上面,如果后台组件想要启动服务不允许,这样的操作导致很多应用都需要适配,改动大量的代码,在这里,如果Google官方给出了三种解决方式,AlarmManager、SyncAdapter、JobScheduler来代替服务,这里主要讲解如何使用这这三种方式如何应用

  • AlarmManager
  • SyncAdapter
  • JobScheduler

2、AlarmManager

AlarmManager通常用途是用来开发手机闹钟,但是AlarmManager的用处并只是这个。AlarmManager其实是一个全局定时器,它可以在指定时间或指定周期启动其他组件(Activity、Service、BroadcastReceiver),Alarm Manager适用于希望在特定时间运行应用程序代码的情况,即使应用程序当前未运行也是如此。对于正常的定时操作(刻度,超时等),使用起来更容易,更有效.

2.1、AlarmManager使用的一些坑

  • 华为手机上kill应用后,无法唤醒Alarm. 大部分原因都是手机为Alarm设置了统一的唤醒时间
  • 不能精确启动闹钟服务 (API19以上是无法精确时间的)
  • 华为手机上休眠无法启动闹钟服务

2.2、AlarmManager的主要方法:

  • set(int type,long triggerAtTime,PendingIntent operation) : 设置指定triggerAtTime时间启动由operation参数指定组件

    type : 定时服务的类型,有以下四种取值

    • ElAPSED_REALTIME: 指定从现在开始过了一定时间后启动operation所对应的组件。
    • ELASPED_REALTIME_WAKEUP: 指定从现在开始时间过了一定时间operation所对应的组件。即使系统处于休眠状态也会执行也会执行operation所对应的组件。
    • RTC: 指定当系统调用System.currentTimeMillis()方法的返回值与triggerAtTime相等时启动operation所对应的组件。
    • RTC_WAKEUP:指定当系统调用System.currentTimeMillis()方法的返回值与triggerAtTime相等时启动operation对应的组件,即使系统休眠状态也会执行operation所对应的组件。
  • setInexactRepeating(int type,long triggerAtTime,long interval,PendingIntent operation) : 设置非精确的周期性任务。例如,我们设置Alarm每个小时启动一次,但是系统不一定总在每个小时的开始启动Alarm服务。

  • setRepeating(int type,long triggerAtTime,long interval,PendingIntent operation) :设置一个周期性执行的定时服务。
  • cancel(PendingIntent operation) :取消AlarmManager的定时服务。

2.3、注意

API 19(Android4.4)开始,AlarmManager的机制是非准确激发的,操作系统会偏移(shift)闹钟来最小化唤醒和电池消耗。不过AlarManager新增了如下两个方法来支持精确激发。

  • setExact(int type long triggerAtMillis,PendingIntent operation) :设置闹钟闹钟将在精确的时间被激发。
  • setWindow(int type,long windowStartMillis,long windowLengthMillis,PendingIntent operation) :设置闹钟将在精确的时间段内被激发。

很显示API19以后无法使用setInexactRepeating()和setRepeating(),也就是无法设置重复闹钟,唯一解决的方式,也只有启动闹钟的时候再设置一次闹钟,也就变相地实现了重复闹钟了。
API19以下使用setExact()和setWindow()将会报没有匹配的方法
java.lang.NoSuchMethodError: android.app.AlarmManager.setExact

解决办法是判断SDK版本,根据SDK版本来定义不同的方法。

1
int sdkVersion = Integer.valueOf(Build.VERSION.SDK_INT);

2.4、做一个小Demo,就是通过使用AlarmManager来做一个闹钟的小Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.setDate).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Calendar currentTime = Calendar.getInstance();
new TimePickerDialog(MainActivity.this, new TimePickerDialog.OnTimeSetListener() {

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
Intent i=new Intent(MainActivity.this,AlarmActivity.class);
//创建PendingIntent对象
PendingIntent pi=PendingIntent.getActivity(MainActivity.this,0,i,0);
Calendar c=Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());

//Calendar.HOUR这是12小时制因为无论你的TimePickerDialog设置的是12还是24,hourOfDay默认获取的是24小时制的
//根据用户的选择的时间来设置Calendar对象 c.set(Calendar.HOUR_OF_DAY,hourOfDay);
c.set(Calendar.MINUTE,minute);
//获取AlarmManager
AlarmManager am= (AlarmManager) getSystemService(ALARM_SERVICE);
Log.i(TAG, "onTimeSet: "+SystemInfoUtil.getSDKVersionNumber());
if (SystemInfoUtil.getSDKVersionNumber()>=19){
//API19以上使用
am.setExact(AlarmManager.RTC_WAKEUP,c.getTimeInMillis(),pi);
}else {
am.set(AlarmManager.RTC_WAKEUP,c.getTimeInMillis(),pi);
}
Toast.makeText(MainActivity.this,"设置闹钟成功", Toast.LENGTH_LONG).show();
}
},currentTime.get(Calendar.HOUR_OF_DAY),currentTime.get(Calendar.MINUTE),false).show();
}
});
}


AlarmActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alarm);
// 加载音乐
try {
// String path = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE).getPath();
AssetFileDescriptor openFd=getAssets().openFd("music.mp3");
AudioPlayer.getInstance().play(openFd);
AudioPlayer.getInstance().setLooping(true);
} catch (IOException e) {
e.printStackTrace();
}
new AlertDialog.Builder(this)
.setTitle("闹钟")
.setTitle("时间到!!!!! ")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AudioPlayer.getInstance().stop();
}
})
.create().show();
}

3、SyncAdapter

https://developer.android.com/training/sync-adapters/creating-sync-adapter

4、JobScheduler

Android 不允许后台服务启动服务,所以官方推荐使用JobScheduler来代替原有的后台监视服务,在很多情况下,应用都可以使用 JobScheduler 作业替换后台服务。 例如,CoolPhotoApp 需要检查用户是否已经从朋友那里收到共享的照片,即使该应用未在前台运行。8.0以前应用使用一种会检查其云存储的后台服务。 为了迁移到 Android 8.0,开发者使用一个计划作业替换了这种后台服务,该作业将按一定周期启动,查询服务器,然后退出

Android 5.0的时候,Google就开始提倡使用该工作分发器来优化电量等资源,随着6.0、7.0、8.0版本的到来,JobScheduler慢慢代替后台服务的运行,JobScheduler到底应该如何使用呢?

开发者需要在稍后的某个时间点或者满足某个特定的条件时去执行某个任务,例如当设备开始充电,或者网络状态连接到wifi状态时执行某些推送通知的任务,jobscheduler就是用来处理这类场景的任务。

Jobscheduler的android在5.0上针对于降低功耗而提出来的一种策略方案,自 Android 5.0 发布以来,JobScheduler 已成为执行后台工作的首选方式,其工作方式有利于用户。应用可以在安排作业的同时允许系统基于设备状态、电源和连接情况等具体条件进行优化。JobScheduler 可实现控制和简洁性,谷歌推出该机制是想要所有应用在执行后台任务时使用它。(还有一点需要注意的是,在7.0上谷歌给出建议:在 Android 7.0 中,删除了三个常用隐式广播 —CONNECTIVITY_ACTION、ACTION_NEW_PICTURE 和ACTION_NEW_VIDEO— 因为这些广播可能会一次唤醒多个应用的后台进程,同时会耗尽内存和电量。如果应用需要收到这些广播,充分利用 Android 7.0 以迁移到 JobScheduler 和相关的 API。

4.1、如何使用Job

应用如果想使用JobScheduler API的话,首先需要创建自己需要执行的任务信息,创建任务的方法在谷歌官方文档上已经有详细介绍,这里只是放出一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);  
ComponentName jobService = new ComponentName(this, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(100012, jobService) //任务Id等于100012
.setMinimumLatency(5000)// 任务最少延迟时间为5s
.setOverrideDeadline(60000)// 任务deadline,当到期没达到指定条件也会开始执行
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)// 需要满足网络条件,默认值NETWORK_TYPE_NONE
.setPeriodic(AlarmManager.INTERVAL_DAY) //循环执行,循环时长为一天(最小为15分钟)
.setRequiresCharging(true)// 需要满足充电状态
.setRequiresDeviceIdle(false)// 设备处于Idle(Doze)
.setPersisted(true) //设备重启后是否继续执行
.setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR) //设置退避/重试策略
.build();
scheduler.schedule(jobInfo);

上面的一个任务需要满足充电状态,并且设备不出于idle状态,并且需要网络处于非计费类型时,会运行该job,自己在构建自己的job 时候需要按需设置触发条件。但是这里需要注意的一个点是JobScheduler所创建并执行的任务必须是带有条件限制的,不然是违背其初衷的,当你创建一个任务,并且不设置任何限制条件并且直接调用 scheduler.schedule(builder.build());去执行该任务是不可行的,会报以下的异常

1
java.lang.IllegalArgumentException: You're trying to build a job with no constraints, this is not allowed.
  1. setMinimumLatency(long minLatencyMillis): 设置任务的最小延迟执行时间(单位是毫秒)。
  2. setOverrideDeadline(long maxExecutionDelayMillis): 设置任务最晚的延迟时间。如果到了规定的时间时其他条件还未满足,你的任务也会被启动。
  3. setPersisted(boolean isPersisted): 设置当设备重启之后该任务是否还要继续执行。
  4. setExtras(PersistableBundle extras): 设置传递bundler参数
  5. setRequiredNetworkType(int networkType):设置需要满足网络类型
  6. setRequiresCharging(boolean requiresCharging):设置是否需要充电状态下运行
  7. setRequiresDeviceIdle(boolean requiresDeviceIdle):设置是否需要在Idle(Doze)状态下运行(7.0上新加的)
  8. addTriggerContentUri(@NonNull TriggerContentUri uri):设置监控ContentUri 发生改变时运行(7.0上新加的)
  9. setPeriodic(long intervalMillis):设置循环执行的时长

具体条件可以参考Google官方网站,

https://developer.android.com/reference/android/app/job/JobScheduler

当需要使用JobScheduler 来干实际的任务时,需要新建一个service,来继承JobService(这一点与DreamService类似),而且必须重写其中的两个方法,分别是onStartJob(JobParameters params)和onStopJob(JobParameters params);
在startJob里来执行自己的代码逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
Log.i("MyTest", "onStartJob @@@@@@@@ " + params);
jobFinished(params,true);
return false;
}

@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}

这里需要注意的一点是,当做完自己的任务要及时调用JobFinished 来结束自己的任务。在6.0上在如果不调用该接口会造成严重的功耗问题。

当上面创建任务时执行到scheduler.schedule(builder.build()); 则开始准备执行任务,一旦满足设置的条件,便会执行到onStartJob()方法,也就是在我们的任务应该具体事宜应该是放在onStartJob中去做的。该方法返回值为为一个boolean值,如果返回值是false,系统假设这个方法返回时任务已经执行完毕,如果返回值是true,那么系统假定这个任务正要被执行,执行任务的重担就落在了你的肩上(这时候需要去新开一个线程去做事物,文档接口上有起描述)。当任务执行完毕后要调用jobFinished()来通知系统。

当系统受到一个cancel请求时会取消该任务(当该任务未执行将其在pending list删除,如果该任务正在执行则停止其任务)。
使用Jobscheduler还需要到AndroidManifest.xml中添加一个service节点让你的应用拥有绑定和使用这个JobService的权限。

<service android:name=”com.example.suansuan.jobtest.MyJobService”
android:permission=”android.permission.BIND_JOB_SERVICE” />

4.2、Job的使用出现的坑

  1. 小心JobService运行在主线程:上面已经说明了,当schedule是在主线程调用的,那么jobsevice则运行则主线程,需要小心主线程ANR 以及严苛模式抛出异常
  2. 小心cancelAll(): 该方法的功能是取消该uid下的所有jobs,也就是说当存在多个app通过shareUid的方式,那么在其中任意一个app执行cancalAll(),则会把所有同一uid下的app中的jobs都cancel掉。
  3. Jobscheduler如果设置了setPersisted(true),则重启后还会再运行,不能使用HashCode 来做JobId
  4. job执行完了,一定要记得要调用JobFinished
  5. 同一个包名下,不能有两个相同的jobId ,如果一个app非常大,两个功能模块为两个程序员维护,则很容易产生沟通不足的情况下,使用了两个相同的JobId,此时只能有一个job生效。另一个无效。该问题可以通过终端命令行: adb shell dumpsys jobscheduler 来查看单个app 的job信息确认

总结

JobScheduler 虽然是在5.0上新增加的一个新服务,但是从L到M,N以及最新的O 上,谷歌Android也是在重点推荐使用该功能,并且在Android O 上谷歌还推出了一套Android vitals 计划,旨在提高Android 系统的功耗,性能,以及稳定性等相关指标,在对功耗上提出来的建议便是,非精确性的定时任务建议使用Job来代替Alarm,能更加准确的满足条件的执行你想要执行的任务。在Android O上JobScheduler更加完善了其条件控制,加上了低存储,低电量策略下的job运行限制,这里将在后面job服务解析中继续提到