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來執行。

源碼下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM