Android7.0以后你不知道的JobService坑

       JobService是什么这里就不多介绍了; JobService是Android5.0以后出来的,随着Android版本的发布,google对系统的性能要求越来越高;从7.0以后对后台系统服务进行了限制(参阅官方的7.0行为变更后台优化);最近在项目中发现了以前写的心跳包机制在某些机型上无法心跳的问题,经过问题排查发现是系统版本的差异,在Android7.0以后JobService无法正常按照设置的执行周期执行后台任务。之前的代码:

public void startHeartbeat(Context context){
        if(mJobScheduler!=null){
            mJobScheduler.cancel(JOB_ID);
        }

        mJobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,new ComponentName(context,JobSchedulerService.class));
        builder.setPeriodic(2*60*1000);//设置执行周期
        builder.setPersisted(false);//设备重启以后是否重新执行任务
        builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);//设置任何网络环境下都可以执行
        builder.setRequiresCharging(false);//设置是否在只有插入充电器的时候执行
        builder.setRequiresDeviceIdle(false);//设置手机系统处于空闲状态下执行

        if(mJobScheduler.schedule(builder.build())== JobScheduler.RESULT_SUCCESS){
            Log.d(TAG,"定时器作业成功");
            List jobInfos = mJobScheduler.getAllPendingJobs();
            if(jobInfos!=null && jobInfos.size()>0){
                for (JobInfo jobInfo:jobInfos){
                    Log.d(TAG,"队里里面的作业:"+jobInfo.getId());
                }
            }
        }else{
            Log.d(TAG,"定时器作业失败");
        }
    }

上面创建JobService的代码在Android7.0以前是正常按照设置的执行周期执行的;但是由于Android官方在7.0以后为了不让应用占用系统太多资源以及电池优化,对定时任务JobService设了限制,强制了执行最小时间周期不能小于15分钟,那是不是这样呢?我们对比一下Android7.0以前跟7.0以后的源码;

官方7.0源码这里我只挑了关键代码。


    /* Minimum interval for a periodic job, in milliseconds. */
    private static final long MIN_PERIOD_MILLIS = 15 * 60 * 1000L;   // 15 minutes


    /**
     * Query the minimum interval allowed for periodic scheduled jobs.  Attempting
     * to declare a smaller period that this when scheduling a job will result in a
     * job that is still periodic, but will run with this effective period.
     *
     * @return The minimum available interval for scheduling periodic jobs, in         milliseconds.
     */
    public static final long getMinPeriodMillis() {
        return MIN_PERIOD_MILLIS;
    }


    /**
     * Set to the interval between occurrences of this job. This value is not set if the
     * job does not recur periodically.
     */
    public long getIntervalMillis() {
        final long minInterval = getMinPeriodMillis();
        return intervalMillis >= minInterval ? intervalMillis : minInterval;
    }

官方6.0源码这里我只挑了关键代码。

    /**
     * Set to the interval between occurrences of this job. This value is not set if     the
     * job does not recur periodically.
     */
    public long getIntervalMillis() {
        return intervalMillis;
    }

而6.0的源码里面并没有最小执行时间周期的限制,而是通过intervalMillis(执行时间间隔)在构建JobInfo的时候进行赋值:

   /** Builder class for constructing {@link JobInfo} objects. */
    public static final class Builder {
        
        // Periodic parameters.
        private boolean mIsPeriodic;
        private boolean mHasEarlyConstraint;
        private boolean mHasLateConstraint;
        private long mIntervalMillis;//执行时间间隔
        /**
         * Specify that this job should recur with the provided interval, not more than once per
         * period. You have no control over when within this interval this job will be executed,
         * only the guarantee that it will be executed at most once within this interval.
         * Setting this function on the builder with {@link #setMinimumLatency(long)} or
         * {@link #setOverrideDeadline(long)} will result in an error.
         * @param intervalMillis Millisecond interval for which this job will repeat.
         */
        public Builder setPeriodic(long intervalMillis) {
            mIsPeriodic = true;
            mIntervalMillis = intervalMillis;
            mHasEarlyConstraint = mHasLateConstraint = true;
            return this;
        }

通过阅读6.0与7.0的源码对比我们可以得出结论并发出思考:

  1.  可以看到7.0的源码系统默认设置了一个最小间隔时间15分钟,在获取执行间隔时,会先比较最小间隔时间和设置的间隔时间,取其中大的那个。所以setPeriodic设置时间小于15分钟是不会生效的。
  2. setPeriodic设置值大于15分钟,Android7.0与Android7.0以下的版本无区别,都是按照设置执行周期正常执行。
  3. 那么问题来了,如果项目中要使用了小于15分钟的执行时间周期,是不是定时任务就无法重复执行了?

既然我们有疑问那就去验证:验证最好的办法就是去实践!!!

private static final long TIME_INTERVAL = 10* 1000;//每隔TIME_INTERVAL毫秒运行一次


public class JobSchedulerService extends JobService {
    private String TAG = JobSchedulerService.class.getSimpleName();
    private static final int NOTIFY_ID = 100;
    public static  final String NOTIFICATION_CHANNEL_ID ="kad_channel_id";

    public JobSchedulerService() {
    }

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.i(TAG,"定时执行任务完成");
        sendNotification();
        jobFinished(jobParameters,false);
        return true;
    }

    private void sendNotification() {
        int id = new Random(System.nanoTime()).nextInt();
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        NotificationCompat.Builder builder ;
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
            //创建渠道通知
            notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_ID,"kad"));
            builder = new NotificationCompat.Builder(this,NOTIFICATION_CHANNEL_ID);
        }else{
            builder = new NotificationCompat.Builder(this,"");
        }
        builder.setChannelId(NOTIFICATION_CHANNEL_ID);
        builder .setDefaults(Notification.DEFAULT_ALL)
                .setContentTitle("定时通知")
                .setContentText("我是定时通知,定时任务已经执行")
                .setAutoCancel(true)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher);
        Notification notification = builder.build();
        notificationManager.notify(id,notification);

    }

    @TargetApi(Build.VERSION_CODES.O)
    private NotificationChannel createNotificationChannel(String channelId, String channelName){
        NotificationChannel channel = new NotificationChannel(channelId,channelName,NotificationManager.IMPORTANCE_DEFAULT);
        channel.setLightColor(Color.RED);//设置小红点颜色
        channel.enableLights(true);//是否在桌面icon右上角展示小红点
        channel.setShowBadge(true);//长按桌面应用图标是否会显示渠道通知
        return channel;
    }

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

如果定时任务执行正常那么就会每隔10s发出通知,控制台打印定时任务执行完成。但是结果没有打印定时任务执行完成,却打印了如下日志:

日志信息

这里我们是不是看到了很熟悉的15m,日志提示设置执行周期小于15m是不会执行的。但是我们又必须要在几分钟(<15m)的间隔内执行定时任务怎么办呢?这里我们可以使用setMinimumLatency(设置任务执行的最小延迟)来达到绕过7.0系统的限制,(这里不详细介绍setMinimumLatency了,不熟悉的同学可以去看api)那话不多说继续撸码:

 mJobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,new ComponentName(context,JobSchedulerService.class));
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
            builder.setMinimumLatency(10*1000);
        }else{
            builder.setPeriodic(10*1000);
        }

 

我们对7.0以后的代码做兼容处理,我们允许代码看效果:我们设置了10s的执行最小延迟时间,结果如预期任务执行了一次;

手机通知栏

控制台输出日志

 

可是,我要的是定时重复执行任务啊!!!这里我分享一个自己的思路,既然官方限制了在7.0以后定时任务执行最小周期为15分钟,如果有小于15分钟的需求可以在执行完任务之后再次创建定时任务(类似递归调用)。直接贴代码:

     @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.i(TAG,"定时执行任务完成");
        sendNotification();
        doSomeThing();
        jobFinished(jobParameters,false);
        return true;
    }

    private void doSomeThing() {
        //TODO 
        
        //重新调用创建定时任务
        HeartbeatUtils.getInstance().startHeartbeat(this);
    }

或许大家会有这样的疑问,为什么我不设置 jobFinished(jobParameters,true)呢?这样不就重复执行了,这里确实是可以重复(应该说是重试)执行,但是只有是在特定场景下定时任务执行失败了,然后再次重复调度上次失败(跟上次失败的执行条件一致)的任务,而且这个调度周期是线性增长的,每次失败后下一次执行的时间间隔就会变长,所有不是执行间隔相同的定时任务。

以上就是我最近在项目中遇到的问题,如有错误请纠正,谢谢!

你可能感兴趣的:(Android)