JobScheduler源碼分析

下面來分析一下JobSchedulerService的源碼露该,看一下我們在自定義的JobService中的那些回調(diào)方法是怎么被調(diào)用的犹撒。
該文章參考了袁輝輝達摩院的文章

JobSchedulerService是在SystemServer啟動的時候被啟動的

SystemServer.java

private void startOtherServices() {
  mSystemServiceManager.startService(JobSchedulerService.class);
}

那我們接下來看一下JobSchedulerService啟動之后都做了哪些工作

JobSchedulerService.java

JobSchedulerService {
    List<StateController> mControllers;
    final JobHandler mHandler;
    final JobSchedulerStub mJobSchedulerStub;
    final JobStore mJobs;
    ...

    public JobSchedulerService(Context context) {
        super(context);

        //添加了幾個StateController
        //這幾個Controller就是對應于我們前面提到的JobInfo.Builder中設置的那幾個觸發(fā)條件
        //我們暫且不關心
        mControllers = new ArrayList<StateController>();
        mControllers.add(ConnectivityController.get(this));
        mControllers.add(TimeController.get(this));
        mControllers.add(IdleController.get(this));
        mControllers.add(BatteryController.get(this));
        mControllers.add(AppIdleController.get(this));

        //一個Handler在主線程分發(fā)消息师脂,也不是很關心
        mHandler = new JobHandler(context.getMainLooper());
        //這是一個Binder的Stub端织阳,具體的調(diào)度工作應該在這里執(zhí)行站削,
        //可以稍后看一下
        mJobSchedulerStub = new JobSchedulerStub();
        //這個JobStore是什么些侍?從名字上來看可能是對我們的Job進行存儲一個內(nèi)存對象
        mJobs = JobStore.initAndGet(this);
    }

    public void onStart() {
        publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
    }
}

既然感覺JobStore是對我們的Job進行存儲的攘蔽,那么我們先看一下使用到的這個JobStore.initAndGet()方法龙屉。

先看一下這個JobStore類的注釋,簡單的翻譯成中文

保存一系列正在被JobScheduler tracking的Job, 這些job根據(jù)他們的引用來進行區(qū)別满俗,所以這個類中所有方法都不應該對這些Job進行拷貝转捕。
所有對該類的操作都必須獲得該類的鎖,該類有兩個方法重要的Runnable,一個是WriteJobsMapToDiskRunnable(用于持久化這些Job),一個是ReadJobMapsFromDiskRunnable(用于反序列化這些Job)

那么我們看他的initAndGet方法唆垃,這個方法是在JobSchedulerService啟動的時候會調(diào)用的方法五芝。

static JobStore initAndGet(JobSchedulerService jobManagerService) {
    //看起來這個類也沒有做什么操作
    //就是初始化了一個JobStore的單例
    //并且還保存了JobSchedulerService的實例
    //看起來這個類也應該對JobScheduler的調(diào)度產(chǎn)生影響
    synchronized (sSingletonLock) {
        if (sSingleton == null) {
            //[見小節(jié)2.6]
            sSingleton = new JobStore(jobManagerService.getContext(),
                    Environment.getDataDirectory());
        }
        return sSingleton;
    }
}

接下來看一下JobStore的構造方法都做了哪些事情

    private JobStore(Context context, File dataDir) {
        mContext = context;
        mDirtyOperations = 0;

        File systemDir = new File(dataDir, "system");
        File jobDir = new File(systemDir, "job");
        jobDir.mkdirs();
        //可以看出JobScheduler會把Job的信息存儲到data/system/job/jobs.xml中
        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
        mJobSet = new ArraySet<JobStatus>();
        //這是一個很重要的方法
        readJobMapFromDisk(mJobSet);
    }

再看readJobMapFromDisk這個方法是怎么從磁盤讀取Job信息的

readJobMapFromDisk最終會調(diào)用到readJobMapImpl這個方法,在解析這個xml文件之前,我們先來看一下這個jobs.xml的文件結構

<job-info version="0">
<job jobid="101" package="con.chico.dong.test" 
  class="com.chico.dong.test.TimingJobService" 
  sourcePackageName="com.chico.dong.test" 
  sourceUserId="0"
  uid="10090" 
  priority="0"
  flags="0">
  <constraints connectivity="true"/>
  <periodic period="86400000" flex="86400000" deadline="1531466690997" delay="1531380290997"/>
  <extras/>
</job>
</job-info>

可以看出這個xml中主要記錄了每一個Job的jobid, JobService的名字辕万,包名枢步,以及觸發(fā)該Job的一些條件信息

接下來再看如何解析這個jobs.xml

private List<JobStatus> readJobMapImpl(FileInputStream fis)
         throws XmlPullParserException, IOException {
     XmlPullParser parser = Xml.newPullParser();
     parser.setInput(fis, StandardCharsets.UTF_8.name());
     ...

     String tagName = parser.getName();
     if ("job-info".equals(tagName)) {
         final List<JobStatus> jobs = new ArrayList<JobStatus>();
         ...
         eventType = parser.next();
         do {
             //解析每一個job的信息
             if (eventType == XmlPullParser.START_TAG) {
                 tagName = parser.getName();
                 if ("job".equals(tagName)) {
                    //從這些job中找出來persisted job,因為只有persisted job才需要在開機的時候重新schedule
                     JobStatus persistedJob = restoreJobFromXml(parser);
                     if (persistedJob != null) {
                         jobs.add(persistedJob);
                     }
                 }
             }
             eventType = parser.next();
         } while (eventType != XmlPullParser.END_DOCUMENT);
         return jobs;
     }
     return null;
 }

主要方法是restorJobFromXml渐尿,看一下他的實現(xiàn)

private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException, IOException {
    JobInfo.Builder jobBuilder;
    int uid;
    jobBuilder = buildBuilderFromXml(parser);
    jobBuilder.setPersisted(true);
    uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
    ...

    buildConstraintsFromXml(jobBuilder, parser);
    //讀取該job的delay和deadline醉途,相對于當前的開機時間
    Pair<Long, Long> elapsedRuntimes = buildExecutionTimesFromXml(parser);
    ...
    //從JobStatus的構造方法可以看出,JobStatus包含了JobInfo信息砖茸,還包含了執(zhí)行情況的信息
    return new JobStatus(jobBuilder.build(), uid,
                elapsedRuntimes.first, elapsedRuntimes.second);
}

從上面的代碼邏輯來看隘擎,就是從xml中讀取job的信息,然后利用這些信息創(chuàng)建JobStatus, JobStatus對象包含了JobInfo信息渔彰,還有該Job的delay,deadline信息嵌屎,用于schedule.

啟動過程的其余部分就不用太關心了,我們只要知道JobSchedulerService啟動過程的時候會從data/system/job/jobs.xml讀取信息恍涂,每一個Job信息生成一個JobStatus對象宝惰,JobStatus對象包含了JobInfo信息,還包含了上一次被調(diào)度時候的delay和deadline信息再沧。

接下來重點就是看一下就是JobSchedulerService的調(diào)度過程了尼夺。先了解一個概念就是JobSchedulerService會維護三個列表,一個列表包含了所有的Job,一個列表包含了到達觸發(fā)條件可以執(zhí)行的Job(這個列表叫做penddingJobs)淤堵,一個列表包含了正在執(zhí)行的Job寝衫。
JobSchedulerService進行調(diào)度的時候,會用到JobSchedulerContext這個類拐邪,該類代表了一個正在執(zhí)行中的Job慰毅,或者說一個正在執(zhí)行中的Job的執(zhí)行環(huán)境。JobSchedulerService在初始化的時候會根據(jù)內(nèi)存情況創(chuàng)建1個或者3個這個類的實例扎阶。每一個正在執(zhí)行的job都會和這個類關聯(lián)汹胃,當一個job執(zhí)行完成或者取消執(zhí)行時,會清空這個類中的信息东臀,JobSchedulerService會一直保存這個類的實例進行復用.

為了不看的頭暈眼花着饥,先明確幾個概念。

我們在使用JobScheduler的時候惰赋,需要自定義一個JobService宰掉,看一下這個JobService的源碼

public abstract class JobService extends Service {
  private JobServiceEngine mEngine;
  pubic final IBinder onBind(Intent intent){
    return mEngin.getBinder();
  }
  public abstract boolean onStartJob(JobParameters params);
  public abstract boolean onStopJob(JobParameters params);
  public final void jobFinished(JobParameters params, boolean needRescheduled){
    mEngine.jobFinished(params, needRescheduled);
  }
}

從上面的代碼寫法可以看出來,JobService包含了一個JobServiceEngine赁濒,這個JobServiceEngine擁有JobSchedulerService的binder,所以可以看做JobService可以做為服務端和JobSchedulerService進行通信

在看我們獲取JobScheduler的方法轨奄,

mJobScheduler = (JobScheduler)mContext.getSystemService(JOB_SCHEDULER_SERVICE);

這個最終會返回一個JobSchedulerImpl對象流部,該對象包含了一個JobSchedulerService的binder戚绕,用于和JobSchedulerService進行binder通信。

所以現(xiàn)在在我們的App中有兩個binder枝冀,一個是JobSerive,這個和JobSchedulerService進行通信的時候是做為服務端存在的耘子,JobSchedulerService會做為client端調(diào)用我們的onStartJobonStopJob方法果漾。
另一個是JobScheduler或者說是JobSchedulerImpl,這個類做為client端接收JobSchedulerService的調(diào)度谷誓。

那么我們再來看JobSchedulerschedule方法绒障,最終會調(diào)用的JobSchedulerServiceschdeuler方法

JobSchedulerService.schedule

    JobStatus jobStatus = new JobStatus(job, uId);
    //當有相同jodid的任務在執(zhí)行的時候先取消該任務
    cancelJob(uId, job.getId());
    //追蹤該任務的狀態(tài)
    startTrackingJob(jobStatus);
    //在system_server的主線程中發(fā)送消息
    mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
    return JobScheduler.RESULT_SUCCESS;

我們分別來看一下在schedule的時候做的操作捍歪,首先calcelJob

    JobStatus toCancel;
    synchronized (mJobs) {
        //根據(jù)uid和jobId找到對象的JobStatus
        toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
    }
    if (toCancel != null) {
        //取消該Job的執(zhí)行
        cancelJobImpl(toCancel);
    }

進入calcelJobImpl

private void cancelJobImpl(JobStatus cancelled) {
    //這個先不管糙臼,等看startTrackingJob的時候在看
    stopTrackingJob(cancelled);
    synchronized (mJobs) {
        //如果要calcel的job正在等待被執(zhí)行变逃,從等待隊列中移除
        mPendingJobs.remove(cancelled);
        //這個方法應該是真正去cancel一個job,從而回調(diào)到我們的onStopJob方法
        stopJobOnServiceContextLocked(cancelled);
    }
}

進入stopJobOnServiceContextLocked()

private boolean stopJobOnServiceContextLocked(JobStatus job) {
    for (int i=0; i<mActiveServices.size(); i++) {
        JobServiceContext jsc = mActiveServices.get(i);
        final JobStatus executing = jsc.getRunningJob();
        //注意這里的判斷條件,只有該job正在執(zhí)行的時候才會繼續(xù)往下走
        //還記得上一篇我們說怎么判斷一個job正在執(zhí)行嗎粟矿?
        //就是那個JobService的onStartJob返回true
        if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
            jsc.cancelExecutingJob();
            return true;
        }
    }
    return false;
}

最終會調(diào)用到JobSchedulerContextdoCancelLocked方法

void doCancelLocked(int arg1, String reason){
  //如果cancel的時候任何已經(jīng)結束了,比如調(diào)用了jobFinished方法
  //則什么都不做
  if(mVerb == VERB_FINISH){
    return;
  }
  handleCancelLocked();
}

這個handleCancelLocked方法最終會通過binder調(diào)用到我們app的onStopJob方法掏秩,這就不詳細分析了哗讥。

我們從上面的cancel過程可以看出杆煞,JobSchedulerService在cancel一個Job的大體思路是:

  1. 將Job從PendingJobs中移除腐泻,這個PendingJob包含了達到觸發(fā)條件但還沒有執(zhí)行的Job
    2.如果在cancel的時候該Job正在被執(zhí)行,則最終會調(diào)用到我們App的onStopJob方法构诚。如果已經(jīng)執(zhí)行完了铆惑,則不會調(diào)用onStopJob方法纸兔。
    等等這只是一部分工作麻车,我們看到在cancel的時候還會調(diào)用stopTrackingJob方法,我們還需要看一下startTrackingJobstopTrackingJob都做了哪些工作棉饶。

JobSchedulerService.startTrackingJob

private void startTrackingJob(JobStatus jobStatus) {
    boolean update;
    boolean rocking;
    synchronized (mJobs) {
        //mJobs保存了所有的job
        update = mJobs.add(jobStatus);
        rocking = mReadyToRock;
    }
    if (rocking) {
        for (int i=0; i<mControllers.size(); i++) {
            //使用各個控制器來監(jiān)聽該job設置的觸發(fā)信息
            StateController controller = mControllers.get(i);
            if (update) {
                controller.maybeStopTrackingJob(jobStatus);
            }
            controller.maybeStartTrackingJob(jobStatus);
        }
    }
}

同理,StopTrackingJob的工作如下岩梳,

private boolean stopTrackingJob(JobStatus jobStatus) {
    boolean removed;
    boolean rocking;
    synchronized (mJobs) {
        //從mJobs中移除該Job
        removed = mJobs.remove(jobStatus);
        rocking = mReadyToRock;
    }
    if (removed && rocking) {
        for (int i=0; i<mControllers.size(); i++) {
            StateController controller = mControllers.get(i);
            //從各個控制器中移除對該Job觸發(fā)信息的監(jiān)聽
            controller.maybeStopTrackingJob(jobStatus);
        }
    }
    return removed;
}

也就是說在cancel一個Job的時候冀值,會將其從mJobs中移除滑蚯,從pendingJobs中移除抵栈,然后如果該job還正在執(zhí)行古劲,則會調(diào)用該App的onStopJob方法,這就是cancel一個Job的流程疤剑。

我們看到在schedule一個Job的時候,最后會發(fā)送一個MSG_CHECK_JOB弯菊,我們看一下JobSchedulerService收到該消息的時候是怎么對我們的Job進行調(diào)度的管钳。

JobSchedulerService$JobHandler

private class JobHandler extends Handler {
    public void handleMessage(Message message) {
        synchronized (mJobs) {
            if (!mReadyToRock) {
                return;
            }
        }
        switch (message.what) {
            case MSG_JOB_EXPIRED: ...                    
                break;
            case MSG_CHECK_JOB:
                synchronized (mJobs) {
                    maybeQueueReadyJobsForExecutionLockedH();
                }
                break;
        }
        maybeRunPendingJobsH();
        removeMessages(MSG_CHECK_JOB);
    }
}

從上面代碼可以看出,在處理MSG_CHECK_JOB這個消息的時候躏仇,會調(diào)用到兩個方法maybeQueueReadyJobsForExecutionLockedmaybeRunPenddingJobs

從名字上來看,第一個方法的作用是判斷該Job是否滿足觸發(fā)條件糟描,如果滿足觸發(fā)條件船响,則把該Job放到pendingJobs

第二個方法判斷pendingJobs中的job是否可以執(zhí)行
主要看一下第二個方法

private void maybeRunPendingJobsH() {
    synchronized (mJobs) {
        if (mDeviceIdleMode) {
            return;
        }
        Iterator<JobStatus> it = mPendingJobs.iterator();
        while (it.hasNext()) {
            JobStatus nextPending = it.next();
            JobServiceContext availableContext = null;
            for (int i=0; i<mActiveServices.size(); i++) {
                JobServiceContext jsc = mActiveServices.get(i);
                final JobStatus running = jsc.getRunningJob();
                if (running != null && running.matches(nextPending.getUid(),
                        nextPending.getJobId())) {
                    availableContext = null;
                    break;
                }
                if (jsc.isAvailable()) {
                    availableContext = jsc;
                }
            }
            if (availableContext != null) {
                //看樣子這個方法是真正執(zhí)行一個Job的方法
                if (!availableContext.executeRunnableJob(nextPending)) {
                    mJobs.remove(nextPending);
                }
                it.remove();
            }
        }
    }
}

看一下executeRunnableJob這個方法见间,該方法真正執(zhí)行一個Job

boolean executeRunnableJob(JobStatus job) {
    synchronized (mLock) {
        if (!mAvailable) {
            return false;
        }

        mRunningJob = job;
        final boolean isDeadlineExpired =
                job.hasDeadlineConstraint() &&
                        (job.getLatestRunTimeElapsed() < SystemClock.elapsedRealtime());
        mParams = new JobParameters(this, job.getJobId(), job.getExtras(), isDeadlineExpired);
        mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();

        mVerb = VERB_BINDING;
        scheduleOpTimeOut();
        final Intent intent = new Intent().setComponent(job.getServiceComponent());
        //還記得我們在使用JobScheduler的時候必須要自定義一個JobService嗎
       //該方法會bind到我們自定義的JobService
       //在這里JobSchedulerService做為client端菱蔬。
        boolean binding = mContext.bindServiceAsUser(intent, this,
                Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND,
                new UserHandle(job.getUserId()));
        if (!binding) {
            mRunningJob = null;
            mParams = null;
            mExecutionStartTimeElapsed = 0L;
            mVerb = VERB_FINISHED;
            removeOpTimeOut();
            return false;
        }
        ...
        mAvailable = false;
        return true;
    }
}

既然是采用bind的方式啟動一個service,那說明JobSchedulerService中肯定有onServiceConnected方法

public void onServiceConnected(ComponentName name, IBinder service) {
    JobStatus runningJob;
    synchronized (mLock) {
        runningJob = mRunningJob;
    }
    if (runningJob == null || !name.equals(runningJob.getServiceComponent())) {
        mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
        return;
    }
    //系統(tǒng)最終會通過這個JobService的代理回季,來通知JobService執(zhí)行onStartJob方法
    this.service = IJobService.Stub.asInterface(service);

    mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget();
}

這樣茧跋,JobScheduler的整個代碼過程就都搞明白了哪亿。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝇棉,一起剝皮案震驚了整個濱河市篡殷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奇瘦,老刑警劉巖耳标,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件次坡,死亡現(xiàn)場離奇詭異砸琅,居然都是意外死亡,警方通過查閱死者的電腦和手機乡革,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門摊腋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沸版,“玉大人,你說我怎么就攤上這事视粮。” “怎么了橙凳?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長岛啸。 經(jīng)常有香客問我钓觉,道長坚踩,這世上最難降的妖魔是什么荡灾? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮瞬铸,結果婚禮上批幌,老公的妹妹穿的比我還像新娘嗓节。我一直安慰自己荧缘,他們只是感情好截粗,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布掰派。 她就那樣靜靜地躺著,像睡著了一般左痢。 火紅的嫁衣襯著肌膚如雪略步。 梳的紋絲不亂的頭發(fā)上描扯,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音趟薄,去河邊找鬼绽诚。 笑死,一個胖子當著我的面吹牛杭煎,可吹牛的內(nèi)容都是我干的恩够。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼羡铲,長吁一口氣:“原來是場噩夢啊……” “哼蜂桶!你這毒婦竟也來了?” 一聲冷哼從身側響起也切,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤扑媚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雷恃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疆股,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年倒槐,在試婚紗的時候發(fā)現(xiàn)自己被綠了旬痹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡导犹,死狀恐怖唱凯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谎痢,我是刑警寧澤磕昼,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站节猿,受9級特大地震影響票从,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滨嘱,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一峰鄙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧太雨,春花似錦吟榴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兜看。三九已至,卻和暖如春狭瞎,著一層夾襖步出監(jiān)牢的瞬間细移,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工熊锭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弧轧,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓碗殷,卻偏偏與公主長得像精绎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亿扁,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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

  • JobScheduler機制簡析 1.概述 JobScheduler主要用于在未來某個時間下滿足一定條件時觸發(fā)執(zhí)行...
    chewbee閱讀 2,198評論 0 5
  • 基于 Android 7.1.1 源碼分析捺典,本文為原創(chuàng),轉載請注明出處从祝,謝謝~~~愛你們的 Coolqi襟己! 前言 ...
    C_Sev閱讀 2,299評論 0 5
  • 主目錄見::Android高級進階知識(這是總目錄索引) ?今天講這篇文章主要是為了接上面一篇來講的,如果大家不知...
    ZJ_Rocky閱讀 1,583評論 0 2
  • 好多人都以好壞分辨人,要么用二元對立的方式去認識毒涧,就非常容易陷入狹隘贮预;要么用事后觀察總結的辦法,就很容易陷入主觀臆...
    鶴一張閱讀 198評論 0 1
  • 加西亞馬爾克斯說他今生的遺憾是沒能為愛而死契讲。我們都為愛而生仿吞,為愛存在。短短一生捡偏,你沒能給我所有唤冈,但你給了我...
    Rrite閱讀 552評論 0 0