JobScheduler的使用

在一篇關(guān)于網(wǎng)絡(luò)優(yōu)化的博文中發(fā)現(xiàn)了這個詞啤斗。我們的項目中并沒有使用到這個東西乐横,但看了一下還是非常好用的,mark一下以后一定會用到吧娘锁。
貼官方文檔牙寞,喜歡自學(xué)的可以過去看看
貼官方Demo,官方給出的例子也是非常好的莫秆,里面用到了Messager间雀,以后會對它進行介紹。


這個類有什么用呢? 我們想象一種情景:
當(dāng)且僅當(dāng)設(shè)備在空閑狀態(tài), 并且使用wifi時, 自動下載新的Apk镊屎。

也許我們可能會這么做: 注冊一個廣播接受者 , 監(jiān)聽屏幕熄滅狀態(tài) , 熄滅之后檢查網(wǎng)絡(luò)狀態(tài) , 然后再在廣播接受者中啟動一個服務(wù)去下載新的apk惹挟。可以是可以缝驳,但是這個會不會過于復(fù)雜了连锯。而且如果出現(xiàn)需求變更......
比如希望在特定情況下再啟動事務(wù)归苍,比如說延遲若干時間之后,或者等手機空閑了再運行运怖,這樣一方面不會在系統(tǒng)資源緊張之時喧賓奪主拼弃,另一方面也起到削峰填谷提高系統(tǒng)效率的作用。對于這些額外的條件要求摇展,Service并不能直接支持吻氧,往往需要加入其他手段,才能較好地滿足相關(guān)的運行條件

  1. 對于延遲時間執(zhí)行咏连,通扯⑺铮考慮利用系統(tǒng)的鬧鐘管理器AlarmManager進行定時管理
  2. 對于是否聯(lián)網(wǎng)、是否充電祟滴、是否空閑振惰,一般要監(jiān)聽系統(tǒng)的相應(yīng)廣播,常見的系統(tǒng)廣播說明如下:
  • 網(wǎng)絡(luò)狀態(tài)變化需要監(jiān)聽系統(tǒng)廣播android.net.conn.CONNECTIVITY_CHANGE踱启;
  • 設(shè)備是否充電需要監(jiān)聽系統(tǒng)廣播Intent.ACTION_POWER_CONNECTED也就是android.intent.action.ACTION_POWER_CONNECTED报账;
  • 設(shè)備是否空閑需要監(jiān)聽系統(tǒng)廣播Intent.ACTION_SCREEN_OFF也就是android.intent.action.SCREEN_OFF研底;

Android從5.0開始埠偿,增加支持一種特殊的機制,即任務(wù)調(diào)度JobScheduler榜晦,該工具集成了常見的幾種運行條件冠蒋,開發(fā)者只需添加少數(shù)幾行代碼,即可完成原來要多種組件配合的工作乾胶,使代碼變得更加優(yōu)(牛)雅(x)抖剿。

認識一下必須要用的工具

  1. JobScheduler: 任務(wù)調(diào)度器
  2. JobInfo : 任務(wù)概要信息
  3. JobService: 任務(wù)服務(wù),描述具體邏輯

簡單使用JobScheduler

  1. 創(chuàng)建JobService
    我們具體的業(yè)務(wù)邏輯還是要寫在jobService中的, 所以自定義一個服務(wù)繼承自JobService 并重寫兩個抽象方法

onStartJob:在任務(wù)開始執(zhí)行時觸發(fā)识窿。返回false表示執(zhí)行完畢斩郎,返回true表示需要開發(fā)者自己調(diào)用jobFinished方法通知系統(tǒng)已執(zhí)行完成。
onStopJob喻频,在任務(wù)停止執(zhí)行時觸發(fā)缩宜。

  1. Activity中配置JobInfo
    JobInfo是從來描述任務(wù)的執(zhí)行時間,條件甥温,策略等一系列的行為锻煌,使用Builder模式來獲取實例,這里摘一下官方給出的代碼
JobInfo.Builder builder = new JobInfo.Builder(mJobId++, mServiceComponent);

        String delay = mDelayEditText.getText().toString();
        if (!TextUtils.isEmpty(delay)) {
            //設(shè)置至少延遲多久后執(zhí)行姻蚓,單位毫秒.  
            builder.setMinimumLatency(Long.valueOf(delay) * 1000);
        }
        String deadline = mDeadlineEditText.getText().toString();
        if (!TextUtils.isEmpty(deadline)) {
            //設(shè)置最多延遲多久后執(zhí)行宋梧,單位毫秒。
            builder.setOverrideDeadline(Long.valueOf(deadline) * 1000);
        }
        boolean requiresUnmetered = mWiFiConnectivityRadioButton.isChecked();
        boolean requiresAnyConnectivity = mAnyConnectivityRadioButton.isChecked();
        if (requiresUnmetered) {
        //設(shè)置需要的網(wǎng)絡(luò)條件狰挡,有三個取值:
        //JobInfo.NETWORK_TYPE_NONE(無網(wǎng)絡(luò)時執(zhí)行捂龄,默認)释涛、
        //JobInfo.NETWORK_TYPE_ANY(有網(wǎng)絡(luò)時執(zhí)行)、
        //JobInfo.NETWORK_TYPE_UNMETERED(網(wǎng)絡(luò)無需付費時執(zhí)行) 
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
        } else if (requiresAnyConnectivity) {  
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
        }
        //是否在空閑時執(zhí)行
        builder.setRequiresDeviceIdle(mRequiresIdleCheckbox.isChecked());
        
        //是否在充電時執(zhí)行 
       builder.setRequiresCharging(mRequiresChargingCheckBox.isChecked());

        // Extras, work duration.
        PersistableBundle extras = new PersistableBundle();
        String workDuration = mDurationTimeEditText.getText().toString();
        if (TextUtils.isEmpty(workDuration)) {
            workDuration = "1";
        }
        extras.putLong(WORK_DURATION_KEY, Long.valueOf(workDuration) * 1000);

        builder.setExtras(extras);

        // Schedule job
        Log.d(TAG, "Scheduling job");
        JobScheduler tm = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        tm.schedule(builder.build());
  • setRequiredNetworkType:設(shè)置需要的網(wǎng)絡(luò)條件跺讯,有三個取值:-
    JobInfo.NETWORK_TYPE_NONE(無網(wǎng)絡(luò)時執(zhí)行枢贿,默認)、JobInfo.NETWORK_TYPE_ANY(有網(wǎng)絡(luò)時執(zhí)行)刀脏、JobInfo.NETWORK_TYPE_UNMETERED(網(wǎng)絡(luò)無需付費時執(zhí)行)
  • setPersisted:重啟后是否還要繼續(xù)執(zhí)行局荚,此時需要聲明權(quán)限RECEIVE_BOOT_COMPLETED,否則會報錯“java.lang.IllegalArgumentException: Error: requested job be persisted without holding RECEIVE_BOOT_COMPLETED permission.”而且RECEIVE_BOOT_COMPLETED需要在安裝的時候就要聲明愈污,如果一開始沒聲明耀态,而在升級時才聲明,那么依然會報權(quán)限不足的錯誤暂雹。
  • setRequiresCharging:是否在充電時執(zhí)行
  • setRequiresDeviceIdle:是否在空閑時執(zhí)行
  • setPeriodic:設(shè)置時間間隔首装,單位毫秒。該方法不能和
    setMinimumLatency杭跪、setOverrideDeadline這兩個同時調(diào)用仙逻,否則會報錯“java.lang.IllegalArgumentException: Can't call setMinimumLatency() on a periodic job”,或者報錯“java.lang.IllegalArgumentException: Can't call setOverrideDeadline() on a periodic job”涧尿。
  • setMinimumLatency:設(shè)置至少延遲多久后執(zhí)行系奉,單位毫秒。
  • setOverrideDeadline:設(shè)置最多延遲多久后執(zhí)行姑廉,單位毫秒缺亮。
  • setBackoffCriteria: 退避策略 , 可以設(shè)置等待時間以及重連策略
  • build:完成條件設(shè)置,返回構(gòu)建好的JobInfo對象桥言。
  1. 聲明MyJobService并將jobinfo加入, 執(zhí)行
    就是上面代碼的最后兩句了, 這里也可以看到JobScheduler 的本質(zhì)其實就是系統(tǒng)的服務(wù)萌踱。

這里吧把官方demo中的JobService拿出來說一下吧,其大致流程就是我們使用JobScheduler的方法号阿。它本質(zhì)上是一個Service

public class MyJobService extends JobService {

    private static final String TAG = MyJobService.class.getSimpleName();

    private Messenger mActivityMessenger;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "Service created");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "Service destroyed");
    }

    /**
     * When the app's MainActivity is created, it starts this service. This is so that the
     * activity and this service can communicate back and forth. See "setUiCallback()"
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mActivityMessenger = intent.getParcelableExtra(MESSENGER_INTENT_KEY);
        return START_NOT_STICKY;
    }

    @Override
    public boolean onStartJob(final JobParameters params) {
        // The work that this service "does" is simply wait for a certain duration and finish
        // the job (on another thread).

        sendMessage(MSG_COLOR_START, params.getJobId());

        long duration = params.getExtras().getLong(WORK_DURATION_KEY);

        // Uses a handler to delay the execution of jobFinished().
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                sendMessage(MSG_COLOR_STOP, params.getJobId());
                jobFinished(params, false);
            }
        }, duration);
        Log.i(TAG, "on start job: " + params.getJobId());

        // Return true as there's more work to be done with this job.
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        // Stop tracking these job parameters, as we've 'finished' executing.
        sendMessage(MSG_COLOR_STOP, params.getJobId());
        Log.i(TAG, "on stop job: " + params.getJobId());

        // Return false to drop the job.
        return false;
    }

    private void sendMessage(int messageID, @Nullable Object params) {
        // If this service is launched by the JobScheduler, there's no callback Messenger. It
        // only exists when the MainActivity calls startService() with the callback in the Intent.
        if (mActivityMessenger == null) {
            Log.d(TAG, "Service is bound, not started. There's no callback to send a message to.");
            return;
        }
        Message m = Message.obtain();
        m.what = messageID;
        m.obj = params;
        try {
            mActivityMessenger.send(m);
        } catch (RemoteException e) {
            Log.e(TAG, "Error passing service object back to activity.");
        }
    }
}

首先看必須重寫的兩個方法:
onStartJob和onStopJob并鸵,

  • 在onStartJob方法中使用Messager發(fā)送了一條消息,Tag為MSG_COLOR_START扔涧, 我們暫時先不管消息發(fā)送到了哪里园担,先宏觀的看一下整個流程。然后發(fā)送了一個延時的消息MSG_COLOR_STOP扰柠,最后調(diào)用jobFinished方法結(jié)束粉铐。
  • 在onStopJob方法中,直接發(fā)送了一條MSG_COLOR_STOP的消息

看來這里發(fā)送消息應(yīng)該是重頭戲了卤档。
首先回到Activity中蝙泼,我們在onStart方法中找到了這樣的代碼

Intent startServiceIntent = new Intent(this, MyJobService.class);
Messenger messengerIncoming = new Messenger(mHandler);
startServiceIntent.putExtra(MESSENGER_INTENT_KEY, messengerIncoming);
startService(startServiceIntent);

在Messager創(chuàng)建的時候就將一個handler作為參數(shù)來構(gòu)造,之后作為extra來啟動了服務(wù)劝枣。所以我們就能在服務(wù)中心的onStartCommand方法中獲取到Messager的實例汤踏。接下來在服務(wù)的sendMessage方法中织鲸,也是直接用到了獲取到的messager來發(fā)送消息,那肯定是發(fā)送到MainActivity傳入的Handler中了溪胶,這里面的代碼就不贅述了搂擦,都懂得。


實現(xiàn)過程的源碼

JobScheduler的創(chuàng)建, 是使用的getSystemService , 我們首先點開JobScheduler類 , 尼瑪居然是一個抽象類? 喵喵喵?怎么辦呢? 可以去翻一下getSystemService 方法 , emm你會很驚喜的
既然是獲取系統(tǒng)服, 按照規(guī)范, 其命名方式一定是xxxService , 所以我們直接去源碼包下面搜JobScheduler, 看看會不會有什么發(fā)現(xiàn)



通過這種方式 , 我們可以找到獲取到的真正的服務(wù): JobSchedulerService
但是問題來了, 我們的App是一個進程 , 系統(tǒng)服務(wù)是在系統(tǒng)的進程, 兩者之間怎么進行通訊呢? 對了 , 進程間通訊 , 一定是用到了AIDL

繼續(xù)在JobSchedulerService中找 , 在第762行會發(fā)現(xiàn)

final class JobSchedulerStub extends IJobScheduler.Stub

那么根據(jù)AIDL的規(guī)范 , 一定有 IJobScheduler對應(yīng)的.aidl文件
打開這個文件 IJobScheduler.aidl :

interface IJobScheduler {
    int schedule(in JobInfo job);
    void cancel(int jobId);
    void cancelAll();
    List<JobInfo> getAllPendingJobs();
}

定義了aidl之后會自動生成相應(yīng)的接口類
既然這個接口的存根(Stub)被繼承 , 自然會重寫接口中的方法 ,這些重寫方法可以在JobSchedulerService$JobSchedulerStub中找到. 所以 , 我們在使用JobScheduler的一些列方法都應(yīng)該是在這里了吧 , 但是JobScheduler和JobScheduler和JobSchedulerService是怎么關(guān)聯(lián)在一起的呢?

因為JobScheduler是一個抽象類 , 必然有實現(xiàn)類 , 按照規(guī)范 , 明明應(yīng)該為xxxImpl. 我們在之前的搜索中已經(jīng)看到過它了 , 接下來點開看看詳情

public class JobSchedulerImpl extends JobScheduler

恩 , 沒毛病就是它了 , 實現(xiàn)類當(dāng)然會實現(xiàn)抽象方法中的抽象方法了
在看構(gòu)造方法 :

IJobScheduler mBinder;

    /* package */ JobSchedulerImpl(IJobScheduler binder) {
        mBinder = binder;
    }

這里甚至直接命名為binder , 和我們綁定服務(wù)的套路一模一樣:
bindService中 , 重寫onServiceConnected , 將傳入的iBinder對象調(diào)用asInterface方法 ,就能獲得aidl對應(yīng)的接口實現(xiàn) , 也就可以調(diào)用對應(yīng)接口實現(xiàn)的方法.

再回到內(nèi)部類JobSchedulerStub 中: 首先我們來看Scheduler方法

@Override
        public int schedule(JobInfo job) throws RemoteException {
            if (DEBUG) {
                Slog.d(TAG, "Scheduling job: " + job.toString());
            }
            final int pid = Binder.getCallingPid();
            final int uid = Binder.getCallingUid();

            enforceValidJobRequest(uid, job);
            if (job.isPersisted()) {
                if (!canPersistJobs(pid, uid)) {
                    throw new IllegalArgumentException("Error: requested job be persisted without"
                            + " holding RECEIVE_BOOT_COMPLETED permission.");
                }
            }

            long ident = Binder.clearCallingIdentity();
            try {
                return JobSchedulerService.this.schedule(job, uid);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

在針對pid和uid進行一系列的驗證之后, 最終還是調(diào)用了外部類的scheduler方法:

/**
     * Entry point from client to schedule the provided job.
     * This cancels the job if it's already been scheduled, and replaces it with the one provided.
     * @param job JobInfo object containing execution parameters
     * @param uId The package identifier of the application this job is for.
     * @return Result of this operation. See <code>JobScheduler#RESULT_*</code> return codes.
     */
public int schedule(JobInfo job, int uId) {
        JobStatus jobStatus = new JobStatus(job, uId);
        cancelJob(uId, job.getId());
        startTrackingJob(jobStatus);
        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
        return JobScheduler.RESULT_SUCCESS;
    }

大家看下注釋中的解釋 , 大致了解一下流程
這里重點說一下startTrackingJob方法

private void startTrackingJob(JobStatus jobStatus) {
        boolean update;
        boolean rocking;
        synchronized (mJobs) {
            update = mJobs.add(jobStatus);
            rocking = mReadyToRock;
        }
        if (rocking) {
            for (int i=0; i<mControllers.size(); i++) {
                StateController controller = mControllers.get(i);
                if (update) {
                    controller.maybeStopTrackingJob(jobStatus);
                }
                controller.maybeStartTrackingJob(jobStatus);
            }
        }
    }

首先將jobStatus添加到JobStore中 , 根據(jù)是否添加成功 , 來決定是否執(zhí)行
controller.maybeStopTrackingJob(jobStatus); 這里也循環(huán)遍歷了控制器 . 乍一看是不是懵逼了 , 什么狗東西?
這里要提一下系統(tǒng)服務(wù)的創(chuàng)建過程了

Zygote--Linux的核心
啟動系統(tǒng)進程SystemServer時 , 就會創(chuàng)建一些關(guān)鍵的服務(wù)哗脖,比如AMS瀑踢,PMS,WMS等才避,其中包括JobSchedulerService橱夭。我們來看一下SystemServer這個類,一共一千多行不是很難桑逝。

首先這是一個java程序棘劣,自然要先去找main函數(shù),然后一步一步跟楞遏。
在run方法中茬暇,我們可以看到一大堆的設(shè)置,往后直接找服務(wù)相關(guān)的(267行)寡喝,三種服務(wù):

startBootstrapServices();
startCoreServices();
startOtherServices();

我們在startOtherServices中可以發(fā)現(xiàn),眾多的服務(wù)都是通過SystemServiceManager去開啟糙俗,它通過類名來反射構(gòu)造器來實例化相應(yīng)的Service,然后添加到系統(tǒng)服務(wù)集合中拘荡,最后啟動服務(wù)臼节。
我們在開發(fā)中使用getSystemService的方式得到的服務(wù)撬陵,就是從剛剛提到了系統(tǒng)服務(wù)集合中獲取的

既然是從構(gòu)造器來實例化服務(wù)的珊皿,所以我們再回到JobSchedulerService看他的構(gòu)造函數(shù),發(fā)現(xiàn)在構(gòu)造函數(shù)中巨税,構(gòu)造器的集合添加了一堆構(gòu)造器蟋定,然后hander分發(fā)事件,讀取本地的文件(data/system/job/jobs.xml)并執(zhí)行任務(wù)草添。
最終在maybeRunPendingJobsH()方法中,調(diào)用了executeRunnableJob方法驶兜,這個方法在JobServiceContext中。該方法中有onServiceConnected方法远寸,即建立鏈接抄淑。同時也有很多很熟悉的方法,就不一一列舉了驰后。

在服務(wù)建立鏈接的同時肆资,還進行防止睡眠等wakeLock操作,emmm灶芝,壞壞的郑原。接下來使用Handler發(fā)送了消息唉韭,what值為MSG_SERVICE_BOUND, 跟進去方法, 最后我們還能看到service.startJob的操作, 即命令服務(wù)執(zhí)行一個任務(wù)。

這樣就將整個流程大致穿起來了犯犁,細節(jié)的地方大家自行查看吧属愤,我覺得基本o98k,看源碼看困了酸役,睡會去- --

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末住诸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涣澡,更是在濱河造成了極大的恐慌只壳,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暑塑,死亡現(xiàn)場離奇詭異吼句,居然都是意外死亡,警方通過查閱死者的電腦和手機事格,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門惕艳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驹愚,你說我怎么就攤上這事远搪。” “怎么了逢捺?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵谁鳍,是天一觀的道長。 經(jīng)常有香客問我劫瞳,道長倘潜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任志于,我火速辦了婚禮涮因,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伺绽。我一直安慰自己养泡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布奈应。 她就那樣靜靜地躺著澜掩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杖挣。 梳的紋絲不亂的頭發(fā)上肩榕,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音程梦,去河邊找鬼点把。 笑死橘荠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的郎逃。 我是一名探鬼主播哥童,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼褒翰!你這毒婦竟也來了贮懈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤优训,失蹤者是張志新(化名)和其女友劉穎朵你,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揣非,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡抡医,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了早敬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忌傻。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖搞监,靈堂內(nèi)的尸體忽然破棺而出水孩,到底是詐尸還是另有隱情,我是刑警寧澤琐驴,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布俘种,位于F島的核電站,受9級特大地震影響绝淡,放射性物質(zhì)發(fā)生泄漏宙刘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一够委、第九天 我趴在偏房一處隱蔽的房頂上張望荐类。 院中可真熱鬧怖现,春花似錦茁帽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饶号,卻和暖如春铁追,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茫船。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工琅束, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扭屁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓涩禀,卻偏偏與公主長得像料滥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子艾船,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容