Android JobService的使用及源码分析

Google在Android 5.0中引入JobScheduler来执行一些需要满足特定条件但不紧急的后台任务,APP利用JobScheduler来执行这些特殊的后台任务时来减少电量的消耗。本文首先介绍JobSerice的使用方法,然后分析JobService的源码实现。

JobService的使用

使用JobScheduler的时候需要把待执行的后台任务封装到JobService中提交。下面就来介绍JobService的使用,首先看一下JobService是什么东东。

从上面的截图,可以看出JobService继承自Service,并且是一个抽象类。在JobService中有两个抽象方法onStartJob(JobParameters)onStopJob(JobParameters)。onStartJob在JobService被调度到的时候会执行,我们只需要继承JobService然后重写onStartJob方法,并在里面执行我们的后台任务就可以了。

下面给出一个JobService的使用实例。

首先,定义一个JobService的子类,如:

public class MyJobService extends JobService {
    public static final String TAG = MyJobService.class.getSimpleName();

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.i(TAG, "onStartJob:" + params.getJobId());
        Toast.makeText(MyJobService.this, "start job:" + params.getJobId(), Toast.LENGTH_SHORT).show();
        jobFinished(params, false);//任务执行完后记得调用jobFinsih通知系统释放相关资源
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.i(TAG, "onStopJob:" + params.getJobId());
        return false;
    }
}

在MyJobService中,onStartJob里面的逻辑非常简单:弹出一个Toast。定义完JobService之后,剩下的工作就是提交Job了,这里我们在Activity中实现,用户点击button来提交任务。Activity的代码如下:

public class MainActivity extends Activity {

    public static final String TAG = MainActivity.class.getSimpleName();
    private int mJobId = 0;

    private EditText mDelayEditText;
    private EditText mDeadlineEditText;
    private RadioButton mWiFiConnectivityRadioButton;
    private RadioButton mAnyConnectivityRadioButton;
    private CheckBox mRequiresChargingCheckBox;
    private CheckBox mRequiresIdleCheckbox;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mDelayEditText = (EditText) findViewById(R.id.delay_time);
        mDeadlineEditText = (EditText) findViewById(R.id.deadline_time);
        mWiFiConnectivityRadioButton = (RadioButton) findViewById(R.id.checkbox_unmetered);
        mAnyConnectivityRadioButton = (RadioButton) findViewById(R.id.checkbox_any);
        mRequiresChargingCheckBox = (CheckBox) findViewById(R.id.checkbox_charging);
        mRequiresIdleCheckbox = (CheckBox) findViewById(R.id.checkbox_idle);
    }

    public void onBtnClick(View view) {
        JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        ComponentName componentName = new ComponentName(MainActivity.this, MyJobService.class);
        JobInfo.Builder builder = new JobInfo.Builder(++mJobId, componentName);

        String delay = mDelayEditText.getText().toString();
        if (delay != null && !TextUtils.isEmpty(delay)) {
            //设置JobService执行的最小延时时间
            builder.setMinimumLatency(Long.valueOf(delay) * 1000);
        }
        String deadline = mDeadlineEditText.getText().toString();
        if (deadline != null && !TextUtils.isEmpty(deadline)) {
            //设置JobService执行的最晚时间
            builder.setOverrideDeadline(Long.valueOf(deadline) * 1000);
        }
        boolean requiresUnmetered = mWiFiConnectivityRadioButton.isChecked();
        boolean requiresAnyConnectivity = mAnyConnectivityRadioButton.isChecked();
        //设置执行的网络条件
        if (requiresUnmetered) {
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
        } else if (requiresAnyConnectivity) {
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
        }
        builder.setRequiresDeviceIdle(mRequiresIdleCheckbox.isChecked());//是否要求设备为idle状态
        builder.setRequiresCharging(mRequiresChargingCheckBox.isChecked());//是否要设备为充电状态

        scheduler.schedule(builder.build());
        Log.i(TAG, "schedule job:" + mJobId);
    }
    //......
    }

这里重点看一下26----55行,在button的单击事件响应中,先通过getSystemService拿到系统的JobScheduler,然后使用JobInfo.Buidler来构造一个后台任务,具体看28----55行。在设置后台任务的参数时,需要特别注意的是:以下五个约束条件我们需要至少指定其中的一个,否则调用JobInfo.Buidler的build方法时会抛异常,导致后台任务构造失败。五个约束条件如下:

1)最小延时

2)最晚执行时间

3)需要充电

4)需要设备为idle(空闲)状态(一般很难达到这个条件吧)

5)联网状态(NETWORK_TYPE_NONE--不需要网络,NETWORK_TYPE_ANY--任何可用网络,NETWORK_TYPE_UNMETERED--不按用量计费的网络)

其实仔细想一想也有道理,其实约束条件决定了JobService在什么时候执行,如果都没指定,系统就不知道在什么来执行我们的JobService了。如果我们的后台任务满足以上的一个或多个条件,就可以考虑是不是应该用JobService来执行。

运行效果如下:

JobService源码分析

JobService内部的运行机制究竟是怎样的?既然继承子Service,那么它至少要重写onStartCommand或者onBind。实际上JobService选择的是重写onBind。为什么使用bind方式呢?上面有提到,JobService是通过JobScheduler来调度,很明显这里会涉及到跨进程通信,如果使用AIDL(当然也可以使用Messenger)就可以很容易实现了。看一下源码:

/** @hide */
public final IBinder onBind(Intent intent) {
    return mBinder.asBinder();
}

很明显,这里采用的是AIDL方式。在看一下mBinder的定义:

/** Binder for this service. */
IJobService mBinder = new IJobService.Stub() {
    @Override
    public void startJob(JobParameters jobParams) {
        ensureHandler();
        Message m = Message.obtain(mHandler, MSG_EXECUTE_JOB, jobParams);
        m.sendToTarget();
    }
    @Override
    public void stopJob(JobParameters jobParams) {
        ensureHandler();
        Message m = Message.obtain(mHandler, MSG_STOP_JOB, jobParams);
        m.sendToTarget();
    }
};

/** @hide */
void ensureHandler() {
    synchronized (mHandlerLock) {
        if (mHandler == null) {
            mHandler = new JobHandler(getMainLooper());
        }
    }
}

从这里可以看到,JobService定义了一个IJobService接口,在这个接口里面定义了startJob和stopJob两个方法来让JobScheduler调度我们的后台任务的执行。这两个方法的实现也很简单,分别发送了MSG_EXECUTE_JOB和MSG_STOP_JOB两个Message。ensureHandler从名字上看,应该就是用来初始化一个Handler吧。看一下源码就知道了:

/** @hide */
void ensureHandler() {
    synchronized (mHandlerLock) {
        if (mHandler == null) {
            mHandler = new JobHandler(getMainLooper());
        }
    }
}

从这里可以看到,在JobService里面定义了一个JobHandler。注意下这里使用的是getMainLooper(),因此,消息是在主线程中处理。继续看JobHandler是怎么处理这两个消息的:

class JobHandler extends Handler {
    JobHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        final JobParameters params = (JobParameters) msg.obj;
        switch (msg.what) {
            case MSG_EXECUTE_JOB:
                try {
                    boolean workOngoing = JobService.this.onStartJob(params);
                    ackStartMessage(params, workOngoing);
                } catch (Exception e) {
                    Log.e(TAG, "Error while executing job: " + params.getJobId());
                    throw new RuntimeException(e);
                }
                break;
            case MSG_STOP_JOB:
                try {
                    boolean ret = JobService.this.onStopJob(params);
                    ackStopMessage(params, ret);
                } catch (Exception e) {
                    Log.e(TAG, "Application unable to handle onStopJob.", e);
                    throw new RuntimeException(e);
                }
                break;
            case MSG_JOB_FINISHED:
                final boolean needsReschedule = (msg.arg2 == 1);
                IJobCallback callback = params.getCallback();
                if (callback != null) {
                    try {
                        callback.jobFinished(params.getJobId(), needsReschedule);
                    } catch (RemoteException e) {
                        Log.e(TAG, "Error reporting job finish to system: binder has gone" +
                                "away.");
                    }
                } else {
                    Log.e(TAG, "finishJob() called for a nonexistent job id.");
                }
                break;
            default:
                Log.e(TAG, "Unrecognised message received.");
                break;
        }
    }
    ......//省略部分代码
 }

从源码中,可以很清楚的看到,在第10----18行,处理在startJob中发出的消息,这里会调用JobService.this.onStartJob(params)来执行任务,在第19----27调用JobService.this.onStopJob(params)来通知我们需要停止任务了。如果我们的后台任务需要在wifi可用的时候才执行的话,如果在任务执行的过程中wifi断开了,那么系统就调用onStopService来通知我们停止运行。

再次强调一下,JobService中的后台任务是在主线程中执行,这里一定不能执行耗时的任务。虽然在JobService中使用了Binder,但是最后还是通过Handler将任务调度到主线程中来执行。

在上面的例子用,有提到在JobInfo.Builder中配置JobService的时候需要指定至少一个约束(触发)条件,否则会抛出异常,这里我们也看一下JobInfo.Builder的build方法:

public JobInfo build() {
    // Allow jobs with no constraints - What am I, a database?
    if (!mHasEarlyConstraint && !mHasLateConstraint && !mRequiresCharging &&
            !mRequiresDeviceIdle && mNetworkType == NETWORK_TYPE_NONE) {
        throw new IllegalArgumentException("You‘re trying to build a job with no " +
                "constraints, this is not allowed.");
    }
    mExtras = new PersistableBundle(mExtras);  // Make our own copy.
    // Check that a deadline was not set on a periodic job.
    if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) {
        throw new IllegalArgumentException("Can‘t call setOverrideDeadline() on a " +
                "periodic job.");
    }
    if (mIsPeriodic && (mMinLatencyMillis != 0L)) {
        throw new IllegalArgumentException("Can‘t call setMinimumLatency() on a " +
                "periodic job");
    }
    if (mBackoffPolicySet && mRequiresDeviceIdle) {
        throw new IllegalArgumentException("An idle mode job will not respect any" +
                " back-off policy, so calling setBackoffCriteria with" +
                " setRequiresDeviceIdle is an error.");
    }
    return new JobInfo(this);
}

从第3----7行,可知,如果5个约束条件都没有指定的时候,会抛出IllegalArgumentException。其实仔细想一想也有道理,其实约束条件决定了JobService在什么时候执行,如果都没指定,系统就不知道在什么来执行我们的JobService了。

总结

最后,总结一下JobService的使用:

1)先继承JobService,并重写startJob和stopJob

2)在manifest.xml中声明JobService的时候,记得一定要加上

android:permission=”android.permission.BIND_JOB_SERVICE”

3)后台任务不能执行耗时任务,如果一定要这么做,一定要再起一个线程去做,使用 thread/handler/AsyncTask都可以。

4)JobService一定要设置至少一个执行条件,如有网络连接、充电中、系统空闲...

5)任务执行完后记得调用jobFinish通知系统释放相关资源

如果我们的后台任务满足JobService的一个或多个约束条件,就可以考虑是不是应该用JobService来执行。

源码下载

时间: 2024-10-03 00:49:21

Android JobService的使用及源码分析的相关文章

Android之rild进程启动源码分析

Android 电话系统框架介绍 在android系统中rild运行在AP上,AP上的应用通过rild发送AT指令给BP,BP接收到信息后又通过rild传送给AP.AP与BP之间有两种通信方式: 1.Solicited Response:Ap向Bp发送请求,Bp给Ap发送回复,该类型的AT指令及其回调函数以数组的形式存放在Ril_commands.h文件中: {数组中的索引号,请求回调函数,响应回调函数} [plain] view plaincopy {0, NULL, NULL},      

Android Small插件化框架源码分析

Android Small插件化框架源码分析 目录 概述 Small如何使用 插件加载流程 待改进的地方 一.概述 Small是一个写得非常简洁的插件化框架,工程源码位置:https://github.com/wequick/Small 插件化的方案,说到底要解决的核心问题只有三个: 1.1 插件类的加载 这个问题的解决和其它插件化框架的解决方法差不多.Android的类是由DexClassLoader加载的,通过反射可以将插件包动态加载进去.Small的gradle插件生成的是.so包,在初始

【Android】Handler、Looper源码分析

一.前言 源码分析使用的版本是 4.4.2_r1. Handler和Looper的入门知识以及讲解可以参考我的另外一篇博客:Android Handler机制 简单而言:Handler和Looper是对某一个线程实现消息机制的重要组成部分,另外两个重要元素是Message和MessageQueue,通过这四个类,可以让某个线程具备接收.处理消息的能力. 二.源码剖析 虽然只有四个类,而且这里只是剖析其中两个,但是也不能独立分析,必须组合进行解析.切入点是类Looper的注释中的一段示例代码: 1

Android异步任务处理框架AsyncTask源码分析

[转载请注明出处:http://blog.csdn.net/feiduclear_up CSDN 废墟的树] 引言 在平时项目开发中难免会遇到异步耗时的任务(比如最常见的网络请求).遇到这种问题,我们可以自己通过Handler+Message+Thread/ThreadPool来构造一个异步耗时任务框架.当你下次项目中又遇到一个网络请求,你又不得不重写异步耗时任务处理框架.出于避免开发者重复搬砖工作,Google工程师给开发者搭建了一个通用的异步耗时任务处理框架--AsyncTask. Asyn

android缓存系列:ASimpleCache源码分析

接触Acache是因为阅读oschina的开源android端代码,发现oschina采用了该框架缓存新闻分页数据.后来知道这是个杨福海的开源项目,他还开源过afinal框架,项目的地址如下: https://github.com/yangfuhai/ASimpleCache 一.官方介绍 ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架.轻量到只有一个java文件(由十几个类精简而来). 1.它可以缓存什么东西? 普通的字符串.JsonObject.JsonArr

Android 资源加载Resources源码分析(8.0)

我们熟悉的资源加载代码: 1.Activity.getResources(); 2.Context.getResources(); 这2种方式获取的都是Resources对象 先看第一种获取Resources对象源码分析: 说明:(AppcompatActivity中getResource()方法与Activity.getResources()是有区别的.AppcompatActivity是new Resources(...)对象) 一:Activity.getResources()源码分析:

Android万能适配器base-adapter-helper的源码分析

项目地址:https://github.com/JoanZapata/base-adapter-helper 1. 功能介绍 1.1. base-adapter-helper base-adapter-helper 是对传统的 BaseAdapter ViewHolder 模式的一个封装.主要功能就是简化我们书写 AbsListView 的 Adapter 的代码,如 ListView,GridView. 1.2 基本使用 mListView.setAdapter(mAdapter = new

Android图片处理神器BitmapFun源码分析

作为一名Android开发人员,相信大家对图片OOM的问题已经耳熟能详了,关于图片缓存和解决OOM的开源项目也是相当的多,被大家熟知的就是Universal_image_loader和Volley了,Volley在前面的文章中已经有介绍.Universal_image_loader在图片缓存功能方面应该算功能最强的,但是感觉很多功能用不上,所以在项目中我一般不太喜欢使用Universal_image_loader(因为本身自己的App源码非常多,加入这些开源库就就更大了,容易出现无法编译的问题,

Android 5.0 Camera系统源码分析(5):Camera预览3A流程

1. 前言 本文分析的是Android Hal层的源码,硬件平台基于mt6735.之前几篇讲的预览流程中3A相关的环节都忽略了,现在重新整理下. 3A指的是Auto Exposure,Auto Focus,Auto White Balance.这三个一起放上来代码实在太多了,这里将重点记录AF的代码.AF的部分工作是由ISP完成的,而ISP的大部分代码mtk都没有开放给我们,比如ISP是如何计算得到对焦位置信息的,但得到对焦位置之后怎么操作对焦马达的代码我们是看得到的,所以涉及到ISP的一些代码