1. 程式人生 > >Android開發筆記(一百四十三)任務排程JobScheduler

Android開發筆記(一百四十三)任務排程JobScheduler

任務排程

App除了通過螢幕向用戶展示可互動的介面元素之外,還經常需要在後臺做些背地裡做的事情,比如說精密計算、檔案下載、統計分析、資料匯入、狀態監控等等,這些使用者看不到的事一般放在Service中處理。

然而有時候我們希望在特定情況下再啟動事務,比如說延遲若干時間之後,或者等手機空閒了再執行,這樣一方面不會在系統資源緊張之時喧賓奪主,另一方面也起到削峰填谷提高系統效率的作用。對於這些額外的條件要求,Service並不能直接支援,往往需要加入其他手段,才能較好地滿足相關的執行條件,比如:
一、對於延遲時間執行,通常考慮利用系統的鬧鐘管理器AlarmManager進行定時管理,有關AlarmManager的說明參見《
Android開發筆記(五十)定時器AlarmManager
》。
二、對於是否聯網、是否充電、是否空閒,一般要監聽系統的相應廣播,常見的系統廣播說明如下:
1、網路狀態變化需要監聽系統廣播android.net.conn.CONNECTIVITY_CHANGE;
2、裝置是否充電需要監聽系統廣播Intent.ACTION_POWER_CONNECTED也就是android.intent.action.ACTION_POWER_CONNECTED;
3、裝置是否空閒需要監聽系統廣播Intent.ACTION_SCREEN_OFF也就是android.intent.action.SCREEN_OFF;

可是要想給Service補充以上條件,勢必加大了程式邏輯的複雜度,一會兒註冊這個事件,一會兒註冊那個事件,工程程式碼將變得不易維護。有鑑於此,Android從5.0開始,增加支援一種特殊的機制,即任務排程JobScheduler,該工具集成了常見的幾種執行條件,開發者只需新增少數幾行程式碼,即可完成原來要多種元件配合的工作。

任務排程機制由三個工具組成,首先是JobInfo,它指定了一個任務的概要資訊,比如何時啟動,啟動時需要滿足什麼條件等等;其次是JobScheduler,它是系統提供的任務排程服務,它的例項從系統服務Context.JOB_SCHEDULER_SERVICE中獲得;最後是JobService,它描述了該任務內部的具體業務邏輯,它的執行時刻由JobScheduler根據JobInfo指定的條件而計算決定。下面分別說明這三個工具的編碼過程:


JobInfo

任務資訊的執行條件由JobInfo.Builder來構造,下面是Builder的函式說明:
建構函式:指定該任務來源與目的,與Intent類似,第二個引數指定了開發者自定義的JobService。
setRequiredNetworkType:設定需要的網路條件,有三個取值:JobInfo.NETWORK_TYPE_NONE(無網路時執行,預設)、JobInfo.NETWORK_TYPE_ANY(有網路時執行)、JobInfo.NETWORK_TYPE_UNMETERED(網路無需付費時執行)
setPersisted:重啟後是否還要繼續執行,此時需要宣告許可權RECEIVE_BOOT_COMPLETED,否則會報錯“java.lang.IllegalArgumentException: Error: requested job be persisted without holding RECEIVE_BOOT_COMPLETED permission.”而且RECEIVE_BOOT_COMPLETED需要在安裝的時候就要宣告,如果一開始沒宣告,而在升級時才宣告,那麼依然會報許可權不足的錯誤。
setRequiresCharging:是否在充電時執行
setRequiresDeviceIdle:是否在空閒時執行
setPeriodic:設定時間間隔,單位毫秒。該方法不能和setMinimumLatency、setOverrideDeadline這兩個同時呼叫,否則會報錯“java.lang.IllegalArgumentException: Can't call setMinimumLatency() on a periodic job”,或者報錯“java.lang.IllegalArgumentException: Can't call setOverrideDeadline() on a periodic job”。
setMinimumLatency:設定至少延遲多久後執行,單位毫秒。
setOverrideDeadline:設定最多延遲多久後執行,單位毫秒。
build:完成條件設定,返回構建好的JobInfo物件。


JobScheduler

任務排程的例項從系統服務Context.JOB_SCHEDULER_SERVICE中獲得,程式碼舉例如下:
		JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
獲得任務排程例項後,即可進行任務排程操作,下面是任務排程的相關方法:
schedule:把指定的JobInfo物件放入排程佇列,並在條件滿足時觸發該物件中定義的JobService。
cancel:取消指定編號的任務。
cancelAll:取消所有任務。
getAllPendingJobs:獲取所有掛起(即尚未執行)的任務。


JobService

任務服務是一種特殊的Service,它描述了該任務內部的具體業務邏輯,需要開發者重寫的方法如下:
onStartJob:在任務開始執行時觸發。返回false表示執行完畢,返回true表示需要開發者自己呼叫jobFinished方法通知系統已執行完成。
onStopJob:在任務停止執行時觸發。

JobService內部另外實現了兩個方法,說明如下

1、onBind方法,原始碼如下所示
    public final IBinder onBind(Intent intent) {
        return mBinder.asBinder();
    }
JobService實現了onBind方法,表示任務排程在工作的時候,JobService是通過繫結方式啟動的。

2、jobFinished方法,原始碼如下所示
    public final void jobFinished(JobParameters params, boolean needsReschedule) {
        ensureHandler();
        Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
        m.arg2 = needsReschedule ? 1 : 0;
        m.sendToTarget();
    }
因為JobService由系統觸發,不是在App的主執行緒中,所以這裡通過Message機制與主執行緒進行通訊。


啟動方式

由於JobService繼承自Service,因此既可以把它當作專門的排程服務來啟動,也可以把它當作普通的服務來啟動。


在Service外部進行排程

在Activity程式碼中增加任務排程,需要宣告JobInfo物件,並通過JobScheduler進行排程,具體程式碼如下所示:
	//將任務作業傳送到作業排程中去
	public void scheduleJob() {
		Log.d(TAG, "scheduleJob");
		JobInfo.Builder builder = new JobInfo.Builder(0, 
				new ComponentName(this, SimpleJobService.class));
		//設定需要的網路條件,預設為JobInfo.NETWORK_TYPE_NONE即無網路時執行
		//NETWORK_TYPE_NONE
		builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
		//builder.setPersisted(true); //重啟後是否還要繼續執行
		builder.setRequiresCharging(false); //是否在充電時執行
		builder.setRequiresDeviceIdle(false); //是否在空閒時執行
		//builder.setPeriodic(1000); //設定時間間隔,單位毫秒
		//setPeriodic不能和setMinimumLatency、setOverrideDeadline這兩個同時呼叫
		//否則會報錯“java.lang.IllegalArgumentException: Can't call setMinimumLatency() on a periodic job”
		//“java.lang.IllegalArgumentException: Can't call setOverrideDeadline() on a periodic job”
		builder.setMinimumLatency(500); //設定至少延遲多久後執行,單位毫秒
		builder.setOverrideDeadline(3000); //設定最多延遲多久後執行,單位毫秒
		JobInfo ji = builder.build();
		
		JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
		js.schedule(ji);
	}

該方式用到的JobService程式碼例子如下所示:
	public class SimpleJobService extends JobService {
	private final static String TAG = "SimpleJobService";

	private Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			JobParameters param = (JobParameters) msg.obj;
			jobFinished(param, true);
			Log.d(TAG, "jobFinished");
		}
	};

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Log.d(TAG, "onStartCommand");
		return START_NOT_STICKY;
	}
	
	@Override
	public boolean onStartJob(JobParameters params) {
		Log.d(TAG, "onStartJob");
		Message message = Message.obtain();
		message.obj = params;
		mHandler.sendMessage(message);
		//返回false表示執行完畢,返回true表示需要開發者自己呼叫jobFinished方法通知系統已執行完成
		return true;
	}

	@Override
	public boolean onStopJob(JobParameters params) {
		//停止,不是結束。jobFinished不會直接觸發onStopJob
		//必須在“onStartJob之後,jobFinished之前”取消任務,才會在jobFinished之後觸發onStopJob
		Log.d(TAG, "onStopJob");
		mHandler.removeMessages(0);
		return true;
	}
	
}

以上程式碼需要注意的是:
1、因為系統服務是通過繫結方式啟動JobService,所以此時onStartCommand方法永遠不會執行;
2、onStopJob顧名思義是在任務停止時觸發,但是直接呼叫jobFinished方法並不能觸發onStopJob。原因是onStopJob的觸發是有條件的,首先這裡的停止指的是取消任務而不是完成任務;其次必須在“onStartJob之後,jobFinished之前”取消任務,才會在jobFinished之後觸發onStopJob。

另外注意在AndroidManifest.xml中補充服務宣告:
        <service
            android:name=".service.SimpleJobService"
            android:permission="android.permission.BIND_JOB_SERVICE" />


在Service內部進行排程

如果Activity通過常規的startService方法啟動JobService,那麼就得JobService自己在onStartCommand方法中進行任務排程了。除了對onStartCommand的處理存在區別之外,其它程式碼與上一種方式基本相同,完整的JobService程式碼如下所示:
public class MultiJobService extends JobService {
	private final static String TAG = "MultiJobService";

	private Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			JobParameters param = (JobParameters) msg.obj;
			jobFinished(param, true);
			Intent intent = new Intent(getApplicationContext(), MainActivity.class);
			intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
			startActivity(intent);
		}
	};

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Log.d(TAG, "onStartCommand");
		scheduleJob();
		return START_NOT_STICKY;
	}

	@Override
	public boolean onStartJob(JobParameters params) {
		Log.d(TAG, "onStartJob");
		Message message = Message.obtain();
		message.obj = params;
		mHandler.sendMessage(message);
		//返回false表示執行完畢,返回true表示需要開發者自己呼叫jobFinished方法通知系統已執行完成
		return true;
	}

	@Override
	public boolean onStopJob(JobParameters params) {
		Log.d(TAG, "onStopJob");
		mHandler.removeMessages(0);
		return true;
	}
	
	//將任務作業傳送到作業排程中去
	public void scheduleJob() {
		Log.d(TAG, "scheduleJob");
		JobInfo.Builder builder = new JobInfo.Builder(0, 
				new ComponentName(this, MultiJobService.class));
	  //設定需要的網路條件,預設為JobInfo.NETWORK_TYPE_NONE即無網路時執行
		builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
		//重啟後是否還要繼續執行,此時需要宣告許可權RECEIVE_BOOT_COMPLETED
		//否則會報錯“java.lang.IllegalArgumentException: Error: requested job be persisted without holding RECEIVE_BOOT_COMPLETED permission.”
		//而且RECEIVE_BOOT_COMPLETED需要在安裝的時候就要宣告,如果一開始沒宣告,在升級時才宣告,那麼依然會報許可權不足的錯誤
		builder.setPersisted(true);
		builder.setRequiresCharging(false); //是否在充電時執行
		builder.setRequiresDeviceIdle(false); //是否在空閒時執行
		builder.setPeriodic(1000); //設定時間間隔,單位毫秒
		JobInfo ji = builder.build();
		
		JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
		js.schedule(ji);
	}

}

另外注意在AndroidManifest.xml中補充服務宣告:
        <service
            android:name=".service.MultiJobService"
            android:permission="android.permission.BIND_JOB_SERVICE" />





點此檢視Android開發筆記的完整目錄