Android JobSchedule漫说

来源

JobSchedule类是从Android 5开始的,已经是快三年前的东西了,最开始提出这个东西,Google的目的是为了节省电量,关于节电部分的讨论,可以移步知乎的一个话题帖 link。

总体上说,这个提出的意义就是将一些不合适的设备唤醒行为进行统一管理,达到省电的目的。

广播

所谓的“隐式广播”

应该并没有显示广播隐式广播的官方说法,使用 显示Intent 发送的就是显示广播,使用 隐式Intent 发送的就是隐式广播。

广播接收器的静态注册、动态注册

在Manifest文件中声明的就是静态注册,代码中通知Context的API进行注册的就是动态注册。静态注册的广播接收器可以在APP进程处于非活状态下,接受广播并激活程序(启动进程)。

鉴于这些特性,有些APP中利用静态注册的形式,监听了一系列系统广播(这些都是通过隐式Intent发出的)。但是,这是比较费电的,举个例子:监听系统新消息广播,提取短信验证码。
这是一个比较常见的需求,一般来说,这个“监听”的生命期应该从调用webservice发出短信开始到相关的Activity结束,但是不排除部分程序员将其做成静态注册的广播接收器。类似的还有网络状态变更的监听(这也是一个很典型的例子,而且更容易设计成静态注册的广播接收器)

这一系列行为使得我们的APP在不需要监听的情况下也进行了监听,甚至是APP进程没有起的情况下,一旦收到广播通知,APP进程就会启动,可能一些依赖网络的信息同步任务、ping包测试、日志统计模块会运行,无形中增加了耗电,但这些是没有意义的。

所以、JobSchedule设计的目的,有一方面是为了减少不合理的静态注册的系统广播接收器。

Android N中的变化

我们知道Android O已经正式发布了,但国内使用Android N的还只有三成,L、M、N形成了三分天下,而且我们知道在去年发布的Android N已经取消了针对三项系统广播的接收器的静态注册的支持。分别是:

  • CONNECTIVITY_ACTION 广播
  • ACTION_NEW_PICTURE广播
  • ACTION_NEW_VIDEO 广播

Android O中的变化

Android O彻底移除了静态注册系统广播的接收器。

其实这是一个很好的改变,意味着我们有更好的方案来优雅的实习需求,毕竟广播是一个重量级的家伙。而且,在此之前,可能你有使用一些方案实现了需求,但在Android O中将不再受到支持,所以是时候把JobSchedule拿出来再细看看了。

JobSchedule的相关说明

先摘录一段Java doc

/**
 * This is an API for scheduling various types of jobs against the framework that will be executed
 * in your application's own process.
 * 

* See {@link android.app.job.JobInfo} for more description of the types of jobs that can be run * and how to construct them. You will construct these JobInfo objects and pass them to the * JobScheduler with {@link #schedule(JobInfo)}. When the criteria declared are met, the * system will execute this job on your application's {@link android.app.job.JobService}. * You identify which JobService is meant to execute the logic for your job when you create the * JobInfo with * {@link android.app.job.JobInfo.Builder#JobInfo.Builder(int,android.content.ComponentName)}. *

*

* The framework will be intelligent about when you receive your callbacks, and attempt to batch * and defer them as much as possible. Typically if you don't specify a deadline on your job, it * can be run at any moment depending on the current state of the JobScheduler's internal queue, * however it might be deferred as long as until the next time the device is connected to a power * source. *

*

You do not * instantiate this class directly; instead, retrieve it through * {@link android.content.Context#getSystemService * Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)}. */

大体说来,包含以下几点信息:

  • 运行在APP进程中
  • 处理多种类型的任务
  • 任务使用JobInfo进行描述
  • 需要创建一个JobService,它是Service的子类
  • 任务的调度是比较灵活的,系统会选择合适的时机处理一批累积的任务
  • JobInfo JobSchedule 的实例化方式

再看一看其源码

public abstract class JobScheduler {
    /**
     * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can occur
     * if the run-time for your job is too short, or perhaps the system can't resolve the
     * requisite {@link JobService} in your package.
     */
    public static final int RESULT_FAILURE = 0;
    /**
     * Returned from {@link #schedule(JobInfo)} if this job has been successfully scheduled.
     */
    public static final int RESULT_SUCCESS = 1;

    /**
     * @param job The job you wish scheduled. See
     * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs
     * you can schedule.
     * @return An int representing ({@link #RESULT_SUCCESS} or {@link #RESULT_FAILURE}).
     */
    public abstract int schedule(JobInfo job);

    /**
     *
     * @param job The job to be scheduled.
     * @param packageName The package on behalf of which the job is to be scheduled. This will be
     *                    used to track battery usage and appIdleState.
     * @param userId    User on behalf of whom this job is to be scheduled.
     * @param tag Debugging tag for dumps associated with this job (instead of the service class)
     * @return {@link #RESULT_SUCCESS} or {@link #RESULT_FAILURE}
     * @hide
     */
    @SystemApi
    public abstract int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag);

    /**
     * Cancel a job that is pending in the JobScheduler.
     * @param jobId unique identifier for this job. Obtain this value from the jobs returned by
     * {@link #getAllPendingJobs()}.
     */
    public abstract void cancel(int jobId);

    /**
     * Cancel all jobs that have been registered with the JobScheduler by this package.
     */
    public abstract void cancelAll();

    /**
     * Retrieve all jobs for this package that are pending in the JobScheduler.
     *
     * @return a list of all the jobs registered by this package that have not
     *         yet been executed.
     */
    public abstract @NonNull List getAllPendingJobs();

    /**
     * Retrieve a specific job for this package that is pending in the
     * JobScheduler.
     *
     * @return job registered by this package that has not yet been executed.
     */
    public abstract @Nullable JobInfo getPendingJob(int jobId);
}

可供使用的API很简单明了,而且都是需要实现类实现的。

刚才我们在API-doc中看到了两个类:JobService和JobInfo。接下来我们看一看有什么门道。

JobInfo

顾名思义,这是描述一个任务的类。

粗略看一下源码,量还是不少的,其实现了序列化(Parcelable接口)、提供了builder(说明创建它还是比较麻烦的)。

从源码中截取了部分,构造时需要初始化的常量:

 private final int jobId;
    private final PersistableBundle extras;
    private final ComponentName service;
    private final boolean requireCharging;
    private final boolean requireDeviceIdle;
    private final TriggerContentUri[] triggerContentUris;
    private final long triggerContentUpdateDelay;
    private final long triggerContentMaxDelay;
    private final boolean hasEarlyConstraint;
    private final boolean hasLateConstraint;
    private final int networkType;
    private final long minLatencyMillis;
    private final long maxExecutionDelayMillis;
    private final boolean isPeriodic;
    private final boolean isPersisted;
    private final long intervalMillis;
    private final long flexMillis;
    private final long initialBackoffMillis;
    private final int backoffPolicy;
    private final int priority;
    private final int flags;

能够描述的任务,在这里可见一斑,以上类成员的具体含义可以参见getter的doc、或者Builder的相关内容,builder中更为详细。

接下来我们就看看Builder中的主要内容:

/**
         * Initialize a new Builder to construct a {@link JobInfo}.
         *
         * @param jobId Application-provided id for this job. Subsequent calls to cancel, or
         * jobs created with the same jobId, will update the pre-existing job with
         * the same id.  This ID must be unique across all clients of the same uid
         * (not just the same package).  You will want to make sure this is a stable
         * id across app updates, so probably not based on a resource ID.
         * @param jobService The endpoint that you implement that will receive the callback from the
         * JobScheduler.
         */
        public Builder(int jobId, ComponentName jobService) {
            mJobService = jobService;
            mJobId = jobId;
        }

        /** @hide */
        public Builder setPriority(int priority) {
            mPriority = priority;
            return this;
        }

        /** @hide */
        public Builder setFlags(int flags) {
            mFlags = flags;
            return this;
        }

        /**
         * Set optional extras. This is persisted, so we only allow primitive types.
         * @param extras Bundle containing extras you want the scheduler to hold on to for you.
         */
        public Builder setExtras(PersistableBundle extras) {
            mExtras = extras;
            return this;
        }

        /**
         * Set some description of the kind of network type your job needs to have.
         * Not calling this function means the network is not necessary, as the default is
         * {@link #NETWORK_TYPE_NONE}.
         * Bear in mind that calling this function defines network as a strict requirement for your
         * job. If the network requested is not available your job will never run. See
         * {@link #setOverrideDeadline(long)} to change this behaviour.
         */
        public Builder setRequiredNetworkType(int networkType) {
            mNetworkType = networkType;
            return this;
        }

        /**
         * Specify that to run this job, the device needs to be plugged in. This defaults to
         * false.
         * @param requiresCharging Whether or not the device is plugged in.
         */
        public Builder setRequiresCharging(boolean requiresCharging) {
            mRequiresCharging = requiresCharging;
            return this;
        }

        /**
         * Specify that to run, the job needs the device to be in idle mode. This defaults to
         * false.
         * 

Idle mode is a loose definition provided by the system, which means that the device * is not in use, and has not been in use for some time. As such, it is a good time to * perform resource heavy jobs. Bear in mind that battery usage will still be attributed * to your application, and surfaced to the user in battery stats.

* @param requiresDeviceIdle Whether or not the device need be within an idle maintenance * window. */
public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) { mRequiresDeviceIdle = requiresDeviceIdle; return this; } /** * Add a new content: URI that will be monitored with a * {@link android.database.ContentObserver}, and will cause the job to execute if changed. * If you have any trigger content URIs associated with a job, it will not execute until * there has been a change report for one or more of them. *

Note that trigger URIs can not be used in combination with * {@link #setPeriodic(long)} or {@link #setPersisted(boolean)}. To continually monitor * for content changes, you need to schedule a new JobInfo observing the same URIs * before you finish execution of the JobService handling the most recent changes.

*

Because because setting this property is not compatible with periodic or * persisted jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called.

* *

The following example shows how this feature can be used to monitor for changes * in the photos on a device.

* * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/PhotosContentJob.java * job} * * @param uri The content: URI to monitor. */
public Builder addTriggerContentUri(@NonNull TriggerContentUri uri) { if (mTriggerContentUris == null) { mTriggerContentUris = new ArrayList<>(); } mTriggerContentUris.add(uri); return this; } /** * Set the delay (in milliseconds) from when a content change is detected until * the job is scheduled. If there are more changes during that time, the delay * will be reset to start at the time of the most recent change. * @param durationMs Delay after most recent content change, in milliseconds. */ public Builder setTriggerContentUpdateDelay(long durationMs) { mTriggerContentUpdateDelay = durationMs; return this; } /** * Set the maximum total delay (in milliseconds) that is allowed from the first * time a content change is detected until the job is scheduled. * @param durationMs Delay after initial content change, in milliseconds. */ public Builder setTriggerContentMaxDelay(long durationMs) { mTriggerContentMaxDelay = durationMs; return this; } /** * 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) { return setPeriodic(intervalMillis, intervalMillis); } /** * Specify that this job should recur with the provided interval and flex. The job can * execute at any time in a window of flex length at the end of the period. * @param intervalMillis Millisecond interval for which this job will repeat. A minimum * value of {@link #getMinPeriodMillis()} is enforced. * @param flexMillis Millisecond flex for this job. Flex is clamped to be at least * {@link #getMinFlexMillis()} or 5 percent of the period, whichever is * higher. */ public Builder setPeriodic(long intervalMillis, long flexMillis) { mIsPeriodic = true; mIntervalMillis = intervalMillis; mFlexMillis = flexMillis; mHasEarlyConstraint = mHasLateConstraint = true; return this; } /** * Specify that this job should be delayed by the provided amount of time. * Because it doesn't make sense setting this property on a periodic job, doing so will * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. * @param minLatencyMillis Milliseconds before which this job will not be considered for * execution. */ public Builder setMinimumLatency(long minLatencyMillis) { mMinLatencyMillis = minLatencyMillis; mHasEarlyConstraint = true; return this; } /** * Set deadline which is the maximum scheduling latency. The job will be run by this * deadline even if other requirements are not met. Because it doesn't make sense setting * this property on a periodic job, doing so will throw an * {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. */ public Builder setOverrideDeadline(long maxExecutionDelayMillis) { mMaxExecutionDelayMillis = maxExecutionDelayMillis; mHasLateConstraint = true; return this; } /** * Set up the back-off/retry policy. * This defaults to some respectable values: {30 seconds, Exponential}. We cap back-off at * 5hrs. * Note that trying to set a backoff criteria for a job with * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build(). * This is because back-off typically does not make sense for these types of jobs. See * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} * for more description of the return value for the case of a job executing while in idle * mode. * @param initialBackoffMillis Millisecond time interval to wait initially when job has * failed. * @param backoffPolicy is one of {@link #BACKOFF_POLICY_LINEAR} or * {@link #BACKOFF_POLICY_EXPONENTIAL} */ public Builder setBackoffCriteria(long initialBackoffMillis, int backoffPolicy) { mBackoffPolicySet = true; mInitialBackoffMillis = initialBackoffMillis; mBackoffPolicy = backoffPolicy; return this; } /** * Set whether or not to persist this job across device reboots. This will only have an * effect if your application holds the permission * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED}. Otherwise an exception will * be thrown. * @param isPersisted True to indicate that the job will be written to disk and loaded at * boot. */ public Builder setPersisted(boolean isPersisted) { mIsPersisted = isPersisted; return this; }

构造函数

需要一个唯一的jobId和用于接受任务回调的Service类路径。注意这个jobID会用于取消任务、同id覆盖,所以粒度应该到实例的级别。

setExtras(PersistableBundle extras)

只接受基础数据类型

setRequiredNetworkType(int networkType)

任务触发的网络状态条件,均为JobInfo中的常量

  • Default. 无网络: NETWORK_TYPE_NONE
  • This job requires network connectivity. 任意网络连接: NETWORK_TYPE_ANY
  • This job requires network connectivity that is unmetered. 不计费的,一般可以认为连接在免费WIFI上:NETWORK_TYPE_UNMETERED
  • This job requires network connectivity that is not roaming.非漫游的蜂窝网络(个人理解就是能用的手机网络,可能有误): NETWORK_TYPE_NOT_ROAMING

setRequiresCharging(boolean requiresCharging)

是否需要处于充电,默认:否

setRequiresDeviceIdle(boolean requiresDeviceIdle)

触发条件需要处于idle状态,默认:否。idle状态:不处于充电+息屏 逐渐进入的一种设备休眠的状态、会限制到网络访问等,有利于省电。

addTriggerContentUri(@NonNull TriggerContentUri uri)

添加一个内容变动监视,注意:不可以和setPeriodic(long)setPersisted(boolean)同时使用。连续的监视需要持续创建新任务。

注意:有一些设置是互斥的

JobService

隶属于package android.app.job,继承自Service

/**
 * 

Entry point for the callback from the {@link android.app.job.JobScheduler}.

*

This is the base class that handles asynchronous requests that were previously scheduled. You * are responsible for overriding {@link JobService#onStartJob(JobParameters)}, which is where * you will implement your job logic.

*

This service executes each incoming job on a {@link android.os.Handler} running on your * application's main thread. This means that you must offload your execution logic to * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result * in blocking any future callbacks from the JobManager - specifically * {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the * scheduling requirements are no longer being met.

*/
  • JobSchedule回调的入口
  • 在onStartJob中实现监听任务的后续逻辑
  • Service是接受到消息是在主线程、所以不要在主线程做业务处理,这会阻塞消息,按照API-doc的说明,必须创建业务线程。
  • onStopJob(android.app.job.JobParameters) 意味着job已经没有用了,假如你有一些特殊的需要连续监视的任务,比如:对content变动的监视,需要在这里创建一个新的job。
  • Service需要在清单中声明,同时声明android.permission.BIND_JOB_SERVICE权限。

大体内容就到这里,可以再写一写实验性的demo。本篇暂不提供demo

你可能感兴趣的:(android)