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