Android JobSchedule漫说

来源:互联网 发布:mac high sierra 失败 编辑:程序博客网 时间:2024/06/06 11:48

来源

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. * <p> * 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)}. * </p> * <p> * 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. * </p> * <p>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<JobInfo> 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.         * <p>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.</p>         * @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.         * <p>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.</p>         * <p>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.</p>         *         * <p>The following example shows how this feature can be used to monitor for changes         * in the photos on a device.</p>         *         * {@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

/** * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p> * <p>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.</p> * <p>This service executes each incoming job on a {@link android.os.Handler} running on your * application's main thread. This means that you <b>must</b> 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.</p> */
  • JobSchedule回调的入口
  • 在onStartJob中实现监听任务的后续逻辑
  • Service是接受到消息是在主线程、所以不要在主线程做业务处理,这会阻塞消息,按照API-doc的说明,必须创建业务线程。
  • onStopJob(android.app.job.JobParameters) 意味着job已经没有用了,假如你有一些特殊的需要连续监视的任务,比如:对content变动的监视,需要在这里创建一个新的job。
  • Service需要在清单中声明,同时声明android.permission.BIND_JOB_SERVICE权限。

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