【Android P】 JobScheduler服務(wù)源碼解析(二)——服務(wù)框架分析

JoScheduler服務(wù)框架分析

前面(一)中基本已經(jīng)App端如何使用Job 服務(wù)做了一個(gè)比較詳細(xì)的介紹,這里將會(huì)對(duì)客戶端幾個(gè)重要類解析;其承擔(dān)的角色;App 從scheduler 一個(gè)job 到執(zhí)行這個(gè)job ,其中服務(wù)端的調(diào)用流程;以及服務(wù)端如何管理系統(tǒng)中所有的job 等等問(wèn)題,將在此做一個(gè)全面的解答。

客戶端

App端從創(chuàng)建一個(gè)job 到調(diào)度一個(gè)Job流程是怎樣的?
Job在客戶端主要比較重要的類有四個(gè):JobInfo,JobScheduler,JobService,JobServiceEngine

public class JobInfo implements Parcelable {
  // 優(yōu)先級(jí)都是內(nèi)部維護(hù)的,APP不可用
  // 默認(rèn)的優(yōu)先級(jí)
  public static final int PRIORITY_DEFAULT = 0;
  // 迅速完成過(guò)的任務(wù)的優(yōu)先級(jí)
  public static final int PRIORITY_SYNC_EXPEDITED = 10;
  // 初始化完成以后的優(yōu)先級(jí)
  public static final int PRIORITY_SYNC_INITIALIZATION = 20;
  // 前臺(tái)任務(wù)的優(yōu)先級(jí)
  public static final int PRIORITY_FOREGROUND_APP = 30;
  // 正在交互任務(wù)的優(yōu)先級(jí)
  public static final int PRIORITY_TOP_APP = 40;
  // 一個(gè)應(yīng)用運(yùn)行的任務(wù)超過(guò)50%時(shí),它的其他任務(wù)的優(yōu)先級(jí)
  public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40;
  // 一個(gè)應(yīng)用運(yùn)行的任務(wù)超過(guò)90%時(shí),它的其他任務(wù)的優(yōu)先級(jí)
  public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80;
  
  
  // 工具類,用于幫助構(gòu)造JobInfo
  public static final class Builder {
    // 構(gòu)造參數(shù)分別是jobId和jobService,jobId是區(qū)分兩個(gè)job的唯一標(biāo)志,提交時(shí),如果jobId相同,會(huì)做更新操作。
    // jobService是任務(wù)運(yùn)行的時(shí)候執(zhí)行的服務(wù),是任務(wù)的具體邏輯
    public Builder(int jobId, ComponentName jobService) {
       mJobService = jobService;
       mJobId = jobId;
    }
  }
  /* 設(shè)置任務(wù)執(zhí)行是所需要的網(wǎng)絡(luò)條件,有三個(gè)參加可選:
     JobInfo.NETWORK_TYPE_NONE(無(wú)網(wǎng)絡(luò)時(shí)執(zhí)行,默認(rèn))
     JobInfo.NETWORK_TYPE_ANY(有網(wǎng)絡(luò)時(shí)執(zhí)行)
     JobInfo.NETWORK_TYPE_UNMETERED(網(wǎng)絡(luò)無(wú)需付費(fèi)時(shí)執(zhí)行)
  */
  public Builder setRequiredNetworkType(int networkType) {
    mNetworkType = networkType;
    return this;
  }
  // 構(gòu)建JobInfo,會(huì)檢查一些合法性問(wèn)題,另外如果沒(méi)有設(shè)置任何條件,也會(huì)報(bào)錯(cuò)
  public JobInfo build() {...}
}

JobInfo 中主要描述了Job 任務(wù)的優(yōu)先級(jí),以及觸發(fā)條件,以及是否循環(huán),系統(tǒng)內(nèi)部還維護(hù)了任務(wù)的優(yōu)先級(jí),在所有符合條件的任務(wù)中,先執(zhí)行優(yōu)先級(jí)高的,后執(zhí)行優(yōu)先級(jí)低的。

public abstract class JobScheduler {
    //job調(diào)度失敗的返回值
    public static final int RESULT_FAILURE = 0;
     
    // job調(diào)度成功的返回值
    public static final int RESULT_SUCCESS = 1;
 
    // 調(diào)度一個(gè)job
    public abstract @Result int schedule(@NonNull JobInfo job);
 
    // enqueue 一個(gè)job 到JobWorkItem 隊(duì)列里
    public abstract @Result int enqueue(@NonNull JobInfo job, @NonNull JobWorkItem work);
 
    // 按照指定包名去調(diào)度一個(gè)job
    @SystemApi
    @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
    public abstract @Result int scheduleAsPackage(@NonNull JobInfo job, @NonNull String packageName,
            int userId, String tag);
 
    // cancel 一個(gè)當(dāng)前包指定的jobId 的job
    public abstract void cancel(int jobId);
 
    // 當(dāng)前UID 對(duì)應(yīng)的包下的全部Job cancel掉,危險(xiǎn)!!!
    public abstract void cancelAll();
 
    // 獲取當(dāng)前已經(jīng)被調(diào)度了的job
    public abstract @Nullable JobInfo getPendingJob(int jobId);
}

JobScheduler 中定義了調(diào)度,cancel ,cancelAll的接口,并且在。在jobSchedulerImpl中實(shí)現(xiàn)

public abstract class JobService extends Service {
    private static final String TAG = "JobService";
 
    public static final String PERMISSION_BIND =
            "android.permission.BIND_JOB_SERVICE";
 
    private JobServiceEngine mEngine;
 
    /** @hide */
    public final IBinder onBind(Intent intent) {
        if (mEngine == null) {
            mEngine = new JobServiceEngine(this) {
                @Override
                public boolean onStartJob(JobParameters params) {
                    return JobService.this.onStartJob(params);
                }
 
                @Override
                public boolean onStopJob(JobParameters params) {
                    return JobService.this.onStopJob(params);
                }
            };
        }
        return mEngine.getBinder();
    }
 
   // 主動(dòng)調(diào)用jobFinished,做最后的Job完成后的清理工作。
    public final void jobFinished(JobParameters params, boolean wantsReschedule) {
        mEngine.jobFinished(params, wantsReschedule);
    }
 
   //在任務(wù)開(kāi)始執(zhí)行時(shí)觸發(fā)。返回false表示執(zhí)行完畢,返回true表示需要開(kāi)發(fā)者自己調(diào)用jobFinished方法通知系統(tǒng)已執(zhí)行完成。
    public abstract boolean onStartJob(JobParameters params);
 
    //在任務(wù)停止執(zhí)行時(shí)觸發(fā)。
    public abstract boolean onStopJob(JobParameters params);
}

JobService 中定義了App中Jobservice 服務(wù)的自身應(yīng)該實(shí)現(xiàn)處理的接口 。

上面打開(kāi)分析了一下App 端的jobScheduler 中的一些類的作用以及角色。代碼量還好,代碼結(jié)構(gòu)也比較清晰。對(duì)于一個(gè)App 去Scheuler 一個(gè)App 的job 時(shí),各個(gè)類的作用以及其角色: 流程圖如下

App端類流程圖.png

根據(jù)上圖的角色來(lái)看:

JobInfo:定義了創(chuàng)建job 的時(shí),指定job 網(wǎng)絡(luò)type, 各種參數(shù),的條件以及Id ,

JobScheduler:類似于PowerManager 的角色,Job_service的客戶端,它的實(shí)例從系統(tǒng)服務(wù)Context.JOB_SCHEDULER_SERVICE中獲得。具體的實(shí)現(xiàn)是在JobSchedulerImpl.java中

JobService: 客戶端需要去實(shí)現(xiàn)的一個(gè)抽象類,主要描述自身的job任務(wù)中應(yīng)該做什么工作

JobServiceEngine:主要的角色是充當(dāng)SystemServer和APP之間的橋梁,負(fù)責(zé)真正調(diào)用onStartJob和onStopJob。JobServiceEngine內(nèi)部有兩個(gè)關(guān)鍵成員mBinder和mHandler重要參數(shù)。

服務(wù)端

JobService服務(wù)的啟動(dòng)流程?

先放一張圖,大概描述一下JobSchedulerService 的啟動(dòng)流程:


啟動(dòng)流程圖.png

下面來(lái)詳細(xì)介紹一下JobSchedulerService 的啟動(dòng)過(guò)程中做了哪些工作。

SystemServer.java
traceBeginAndSlog("StartJobScheduler");
mSystemServiceManager.startService(JobSchedulerService.class);
traceEnd()
 
 
JobSchedulerService.java
public JobSchedulerService(Context context) {
        super(context);
        //mHandler = new JobHandler(context.getMainLooper())  // 吧jobHandler 放到SystemServer 主線程移運(yùn)行
        mConstants = new Constants(mHandler);   // 初始化一些常量
        mJobSchedulerStub = new JobSchedulerStub();  // Binder服務(wù)端的localService
        mJobs = JobStore.initAndGet(this);    // 初始化JobStore ,Jobstore 是干嘛的呢,主要是存放Job 任務(wù)的一個(gè)列表,并記錄Job 的運(yùn)行情況,時(shí)長(zhǎng)等詳細(xì)信息到/data/system/job/jobs.xml
 
        // Create the controllers.
        mControllers = new ArrayList<StateController>();
        mControllers.add(ConnectivityController.get(this));
...
        mControllers.add(DeviceIdleJobsController.get(this)); // 初始化各種controller ,每個(gè)controller 都對(duì)應(yīng)著JobInfo 里面的一個(gè)set 的Job運(yùn)行條件,目前共有連接,時(shí)間,idle(設(shè)備空閑),充電,存儲(chǔ),AppIdle,ContentObserver,DeviceIdle(Doze) 的控制器
 
        // If the job store determined that it can't yet reschedule persisted jobs,
        // we need to start watching the clock.
        if (!mJobs.jobTimesInflatedValid()) {  // 時(shí)間不正確,沒(méi)有初始化完成,那么要注冊(cè)ACTION_TIME_CHANGED 廣播接收器,來(lái)重新設(shè)定一下系統(tǒng)中的job 了
            Slog.w(TAG, "!!! RTC not yet good; tracking time updates for job scheduling");
            context.registerReceiver(mTimeSetReceiver, new IntentFilter(Intent.ACTION_TIME_CHANGED));
        }
    }

構(gòu)造函數(shù)中就是初始化各種Controller 以及存儲(chǔ)器JobStore

@Override
    public void onStart() {
        publishLocalService(JobSchedulerInternal.class, new LocalService());
        publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
    }
 
    @Override
    public void onBootPhase(int phase) {
        if (PHASE_SYSTEM_SERVICES_READY == phase) {
 
            final IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);  // 卸載包
            filter.addAction(Intent.ACTION_PACKAGE_CHANGED);  //升級(jí)包
            filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); //force stop 進(jìn)程
            filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);  // 數(shù)據(jù)變化需要重啟該包進(jìn)程的廣播,例如包名變化
            filter.addDataScheme("package");
            getContext().registerReceiverAsUser(
                    mBroadcastReceiver, UserHandle.ALL, filter, null, null); // 注冊(cè)監(jiān)聽(tīng)廣播
            final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED); //用戶移除。
            getContext().registerReceiverAsUser(
                    mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
            mPowerManager = (PowerManager)getContext().getSystemService(Context.POWER_SERVICE);
            ActivityManager.getService().registerUidObserver(mUidObserver,
                        ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE
                        | ActivityManager.UID_OBSERVER_IDLE, ActivityManager.PROCESS_STATE_UNKNOWN,
                        null);  // 注冊(cè)UID 狀態(tài)變化observer
   
            // Remove any jobs that are not associated with any of the current users.
            cancelJobsForNonExistentUsers();
        } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
            synchronized (mLock) {
                // Create the "runners".
                for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {  // 一次最多只能同時(shí)run 16個(gè)job
                    mActiveServices.add(
                            new JobServiceContext(this, mBatteryStats, mJobPackageTracker,
                                    //getContext().getMainLooper()));
                }
                // Attach jobs to their controllers.
                mJobs.forEachJob(new JobStatusFunctor() { // 將系統(tǒng)中已經(jīng)設(shè)置的所有job 添加controller控制器
                    @Override
                    public void process(JobStatus job) {
                        for (int controller = 0; controller < mControllers.size(); controller++) {
                            final StateController sc = mControllers.get(controller);
                            sc.maybeStartTrackingJobLocked(job, null);
                        }
                    }
                });
            }
        } else if (phase == PHASE_BOOT_COMPLETED) {
            mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
        // END
        }
    }

onStart 的方法里面監(jiān)聽(tīng)了Package 的一些廣播,以及注冊(cè)了UID 的observer , 將Job 的Controller 也給添加上。發(fā)送MSG_CHECK_JOB 消息
大致工作如下:

啟動(dòng)工作.png

App 創(chuàng)建一個(gè)Job并scheduler 它,服務(wù)端流程如何?

當(dāng)使用JobScheduler使用首先會(huì)創(chuàng)建一個(gè)jobscheduler的服務(wù)對(duì)象,當(dāng)調(diào)用scheduler.schedule(builder.build());時(shí)候 scheduler() 便開(kāi)始啟動(dòng)該job 的任務(wù),但是不一定立馬執(zhí)行。

其實(shí)是由JobSchedulerImpl通過(guò)Binder調(diào)用到JobSchedulerService的schedule()中

uid  = Binder.getCallingUid();
  
public int schedule(JobInfo job, int uId) {
    .
    .    // 判斷權(quán)限JobService.PERMISSION_BIND
    .
    return scheduleAsPackage(job, uId, null, -1, null);
}
    public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
            int userId, String tag) {
            if (ActivityManager.getService().isAppStartModeDisabled(uId,
                    job.getService().getPackageName())) {  //這里通過(guò)AMS來(lái)判斷packageName該應(yīng)該是否允許啟動(dòng)該服務(wù),
   
                return JobScheduler.RESULT_FAILURE;
            }
  
        synchronized (mLock) {
            final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());  // 判斷系統(tǒng)中該Uid 的App對(duì)應(yīng)的Jobid 是否已經(jīng)存在系統(tǒng)中
 
            JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);  // 創(chuàng)建一個(gè)對(duì)應(yīng)的JobStatus,并且指定相關(guān)的條件!!!!
            if (ENFORCE_MAX_JOBS && packageName == null) {
                if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) {   // 每個(gè)UID 對(duì)應(yīng)的Job 最多不能同時(shí)存在系統(tǒng)中100個(gè)
                    Slog.w(TAG, "Too many jobs for uid " + uId);
                    throw new IllegalStateException("Apps may not schedule more than "
                                + MAX_JOBS_PER_APP + " distinct jobs");
                }
            }
 
            // This may throw a SecurityException.
            jobStatus.prepareLocked(ActivityManager.getService()); //準(zhǔn)備
 
            if (toCancel != null) {
                cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app");  // 如果系統(tǒng)中已經(jīng)存在了同一個(gè)uid 里面的同一個(gè)jobId 的job ,那么先cancle 這個(gè)job
            }
            if (work != null) {
                // If work has been supplied, enqueue it into the new job.
                jobStatus.enqueueWorkLocked(ActivityManager.getService(), work);  //如果執(zhí)行了work隊(duì)列那么 將jobStatus 放入指定的work隊(duì)列里
            }
            startTrackingJobLocked(jobStatus, toCancel);  // 開(kāi)始將App 的所有的Job的放到mJobs里表里,如果并且對(duì)每個(gè)job 指定對(duì)應(yīng)的不同Controller
 
 
            if (isReadyToBeExecutedLocked(jobStatus)) { 如果一個(gè)job 滿足一定條件需要立即執(zhí)行,那么會(huì)將其放在pending 列表中,并且在后面馬上處理
                // This is a new job, we can just immediately put it on the pending
                // list and try to run it.
                mJobPackageTracker.notePending(jobStatus);
                addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator);
                maybeRunPendingJobsLocked();
            }
        }
        return JobScheduler.RESULT_SUCCESS;
    }

當(dāng)創(chuàng)建一個(gè)job任務(wù)的時(shí)候,會(huì)先判斷該package的啟動(dòng)服務(wù)權(quán)限,并且JobScheduler中維持了一個(gè)mJobs的列表保存這系統(tǒng)中所有的Job任務(wù),當(dāng)新創(chuàng)建一個(gè)job任務(wù)的時(shí)候會(huì)先判斷當(dāng)前系統(tǒng)中是否存在一個(gè)已有的job,如果存在的話,先將其cancel。 然后將該開(kāi)始tracking 該job ,為其指定對(duì)應(yīng)JobController

private void startTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
    if (!jobStatus.isPreparedLocked()) {
        Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
    }
    jobStatus.enqueueTime = SystemClock.elapsedRealtime();
    final boolean update = mJobs.add(jobStatus);
    if (mReadyToRock) {
        for (int i = 0; i < mControllers.size(); i++) {
            StateController controller = mControllers.get(i);
            if (update) {
                controller.maybeStopTrackingJobLocked(jobStatus, null, true);
            }
            controller.maybeStartTrackingJobLocked(jobStatus, lastJob); // 開(kāi)始Tracking 該Job,并指定對(duì)應(yīng)的Controller,JobStatus 中也初始化對(duì)應(yīng)的條件
        }
    }
}

當(dāng)開(kāi)始scheduler 一個(gè)job 的時(shí)候,會(huì)調(diào)用startTrackingJobLocked 方法,將當(dāng)前的時(shí)間賦值給enqueueTime,并且會(huì)先判斷系統(tǒng)mJobs列表中是否已經(jīng)存在這個(gè)job ,如果存在,那么會(huì)先刪掉這個(gè)job 然后重新添加上,繼而調(diào)用controller 的
maybeStartTrackingJobLocked方法開(kāi)始Tracking 該job。

從源碼里看到scheduler 一個(gè)job 流程其實(shí)并不難,調(diào)用的交互類也很少也不多,JSS,JSI,JSC,JobStore等。但是該流程僅僅是將Job加到一個(gè)mJobs列表中,以及指定對(duì)應(yīng)的StateController

其大概流程圖如下(圖片來(lái)自Gityuan):


流程圖.png

Job設(shè)定好了之后如何去觸發(fā)它呢

首先我們知道JobScheduler 中維持了8個(gè)Controller,分別是:AppIdleController.java(App Idle 狀態(tài)控制器),BatteryController.java(電池狀態(tài)控制器),ConnectivityController.java (網(wǎng)絡(luò)連接狀態(tài)控制器),ContentObserverController.java(content監(jiān)聽(tīng)狀態(tài)控制器),DeviceIdleJobsController.java(Doze狀態(tài)控制器),StorageController.java(存儲(chǔ)狀態(tài)控制器),TimeController.java(時(shí)間控制器),IdleController.java(設(shè)備空閑狀態(tài)控制器)。這些controller 控制器主要是控制對(duì)應(yīng)Job 需要滿足的條件是否滿足,是否所有條件都滿足,才允許執(zhí)行。比如說(shuō):我一個(gè)App 如下圖設(shè)置一個(gè)Job

JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, sIdleService)
        .setRequiresDeviceIdle(true) // 需要Device idle 狀態(tài)下才能執(zhí)行
        .setMinimumLatency(15* 60 * 1000); //設(shè)置延遲15min,在N 上有限制,最小延遲時(shí)間不得小小于15分鐘,如果小于15分鐘,則會(huì)主動(dòng)將你的 MinimumLatency修改為15分鐘。
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 需要網(wǎng)絡(luò)非計(jì)費(fèi)類型
        .setRequiresCharging(true);   // 需要充電狀態(tài)
 
 
js.schedule(builder.build());

這里能看到,這個(gè)job 設(shè)置了四個(gè)條件,那么對(duì)應(yīng)到服務(wù)端模塊,從接口上來(lái)看就會(huì)有TimeController.java(時(shí)間控制器),ConnectivityController.java (網(wǎng)絡(luò)連接狀態(tài)控制器),BatteryController.java(電池狀態(tài)控制器),IdleController.java(設(shè)備空閑狀態(tài)控制器)四個(gè)控制器來(lái)監(jiān)控這個(gè)設(shè)備狀態(tài),當(dāng)四個(gè)狀態(tài)都滿足了,也就是:設(shè)備處于空閑狀態(tài),從開(kāi)始調(diào)度到現(xiàn)在已經(jīng)過(guò)了超過(guò)15分鐘,手機(jī)網(wǎng)絡(luò)處于非計(jì)費(fèi)網(wǎng)絡(luò),手機(jī)處于充電狀態(tài)。那么job 任務(wù)就可以執(zhí)行了。

但是反饋到system server 它是怎么運(yùn)行的呢?

首先我們要需要介紹一下Job 創(chuàng)建時(shí),如何和Controller 綁定起來(lái)的。我們?cè)?.1 中有提到,在 scheduleAsPackage 方法中會(huì)通過(guò)JobInfo 創(chuàng)建對(duì)應(yīng)JobStatus對(duì)象。

public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePackageName,
        int sourceUserId, String tag) {
    final long elapsedNow = SystemClock.elapsedRealtime();
    final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
    if (job.isPeriodic()) {  //如果job 是循環(huán)的
        latestRunTimeElapsedMillis = elapsedNow + job.getIntervalMillis(); // 最晚執(zhí)行時(shí)間 = 當(dāng)前時(shí)間+ 循環(huán)間隔
        earliestRunTimeElapsedMillis = latestRunTimeElapsedMillis - job.getFlexMillis(); //最早執(zhí)行時(shí)間 = 最晚觸發(fā)時(shí)間 - Flex(靈活窗口)時(shí)間 , Flex時(shí)間是三個(gè)時(shí)間(5min,5 * interval / 100, 設(shè)置的flexMillis時(shí)間)中最大值
    } else {
        earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ? // job 有最早執(zhí)行限制(默認(rèn)有),則 最早執(zhí)行時(shí)間=當(dāng)前時(shí)間+最小延遲
                elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME;
        latestRunTimeElapsedMillis = job.hasLateConstraint() ?    // job 有最晚執(zhí)行限制(默認(rèn)有),則最晚執(zhí)行時(shí)間=當(dāng)前時(shí)間+最大延遲
                elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME;
    }
    return new JobStatus(job, callingUid, sourcePackageName, sourceUserId, tag, 0,  // 通過(guò)JobInfo,以及執(zhí)行時(shí)間參數(shù)創(chuàng)建JobStatus
            earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
            0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */);
}

在創(chuàng)建用JobInfo builder 一個(gè)job 的時(shí)候,用constraintFlags 變量去使用置位的方式標(biāo)記該job 的一個(gè)條件限制,當(dāng)設(shè)置一個(gè)限制條件,則在相應(yīng)的位上置1。最終形成的JobInfo 對(duì)象中包含了該參數(shù),在創(chuàng)建JobStatus 的時(shí)機(jī)作為Controller 的綁定判斷條件。

 private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
            int sourceUserId, String tag, int numFailures, long earliestRunTimeElapsedMillis,
            long latestRunTimeElapsedMillis, long lastSuccessfulRunTime, long lastFailedRunTime) {
        this.job = job;
...
       int requiredConstraints = job.getConstraintFlags();  //從JobInfo 中獲取constraintFlags 變量來(lái)作為requiredConstraints 賦值給JobStatus 中的requiredConstraints參數(shù)
        switch (job.getNetworkType()) {  //對(duì)于網(wǎng)絡(luò)type限制 ,指定為不同的CONSTRAINT 位
            case JobInfo.NETWORK_TYPE_NONE:
                // No constraint.
                break;
            case JobInfo.NETWORK_TYPE_ANY:
                requiredConstraints |= CONSTRAINT_CONNECTIVITY;
                break;
            case JobInfo.NETWORK_TYPE_UNMETERED:
                requiredConstraints |= CONSTRAINT_UNMETERED;
                break;
            case JobInfo.NETWORK_TYPE_NOT_ROAMING:
                requiredConstraints |= CONSTRAINT_NOT_ROAMING;
                break;
            case JobInfo.NETWORK_TYPE_METERED:
                requiredConstraints |= CONSTRAINT_METERED;
                break;
            default:
                Slog.w(TAG, "Unrecognized networking constraint " + job.getNetworkType());
                break;
        }
 
        //對(duì)于不同時(shí)間type限制 ,指定為不同的CONSTRAINT 位
        if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) {
            requiredConstraints |= CONSTRAINT_TIMING_DELAY;
        }
        if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
            requiredConstraints |= CONSTRAINT_DEADLINE;
        }
        if (job.getTriggerContentUris() != null) {
            requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER;
        }
        this.requiredConstraints = requiredConstraints;  // 將包裝后的requiredConstraints 賦值給JobStatus的變量requiredConstraints。
...
}

JobScheduler 服務(wù)這里總共有8個(gè)Controller ,比較特殊的有:AppIdleController.java 和 DeviceIdleJobsController.java,這兩個(gè)controller 和其他6個(gè)不一樣,他們是只要設(shè)置了job ,便都受這兩個(gè)Controller 的控制。這個(gè)后面會(huì)詳細(xì)說(shuō)明。大部分都是類似的,以其中一個(gè)Controller 為例

BatteryController.java,來(lái)理解其他的Controller

當(dāng)設(shè)置一個(gè)Job 的時(shí)候,會(huì)循環(huán)所有的Controller 來(lái)為其指定限制該Job 的Controller

@Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
    if (taskStatus.hasPowerConstraint()) {//該Job 是否有設(shè)置Power限制,
        mTrackedTasks.add(taskStatus);   // 將該Job 加入到mTrackedJobs 列表中
        taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY); // 該jobStatus開(kāi)始tracking 電池狀態(tài)
        taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());  // 設(shè)置當(dāng)前是否滿足充電狀態(tài)
        taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow()); // 設(shè)置當(dāng)前是否滿足低電狀態(tài)
    }
}
  
  
     // 注冊(cè)監(jiān)聽(tīng)Battery 的一些廣播,在此接受廣播
    @VisibleForTesting
    public void onReceiveInternal(Intent intent) {
        synchronized (mLock) {
            final String action = intent.getAction();
  
          ...
          if (BatteryManager.ACTION_CHARGING.equals(action)) { // 接受到廣播會(huì)將mCharging 置為對(duì)應(yīng)的狀態(tài),并重新檢查一下當(dāng)前狀態(tài)是否滿足job 條件
                if (DEBUG) {
                    Slog.d(TAG, "Received charging intent, fired @ "
                            + SystemClock.elapsedRealtime());
                }
                mCharging = true;
                maybeReportNewChargingStateLocked();
            } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
                if (DEBUG) {
                    Slog.d(TAG, "Disconnected from power.");
                }
                mCharging = false;
                maybeReportNewChargingStateLocked();
            }
  
        }
  
private void maybeReportNewChargingStateLocked() {
    ...
    if (stablePower || batteryNotLow) {
        // If one of our conditions has been satisfied, always schedule any newly ready jobs.
        mStateChangedListener.onRunJobNow(null);   // 條件滿足立即執(zhí)行job
    } else if (reportChange) {
        // Otherwise, just let the job scheduler know the state has changed and take care of it
        // as it thinks is best.
        mStateChangedListener.onControllerStateChanged(); //開(kāi)始狀態(tài)已經(jīng)發(fā)生改變,重新檢查job 是否需要調(diào)度
    }
}

從BatteryController 這個(gè)Controller 大概能窺見(jiàn)JobScheduler 的控制器具體實(shí)現(xiàn)邏輯,控制器的邏輯大概如此:

1.在JobSchedulerService schduler一個(gè)job 的時(shí)候在startTrackingJobLocked()方法中,輪詢了所有的Controller調(diào)用maybeStartTrackingJobLocked。

  1. maybeStartTrackingJobLocked 方法內(nèi)會(huì)首先如BatteryController(hasPowerConstraint)判斷是否 對(duì)應(yīng)Controller 的限制條件

  2. 在對(duì)應(yīng)Controller 創(chuàng)建的時(shí)候注冊(cè)對(duì)應(yīng)設(shè)備狀態(tài)發(fā)生改變的廣播或者Listener(比BatteryController,便是監(jiān)聽(tīng)Battery相關(guān)的廣播更新?tīng)顟B(tài)參數(shù),ConnectivityController便是在注冊(cè)ConnectivityManager.OnNetworkActiveListener,當(dāng)連接狀態(tài)發(fā)生改變時(shí)候,更新?tīng)顟B(tài)參數(shù))

  3. 在當(dāng)對(duì)應(yīng)Controller 接收到設(shè)備狀態(tài)發(fā)生改變會(huì)執(zhí)行:

     1). 判斷當(dāng)前mTackingJobs列表中是否有ready的job,如果有執(zhí)行 mStateChangedListener.onRunJobNow(js) 來(lái)立即執(zhí)行這個(gè)job(Battery,Connectivity,Time),
    
     2). 判斷當(dāng)前狀態(tài)已經(jīng)發(fā)生改變,那么調(diào)用到mStateChangedListener.onControllerStateChanged() 來(lái)回調(diào)到JobschedulerService中。
    
  4. JobschedulerService收到回調(diào),發(fā)送消息MSG_CHECK_JOB 來(lái)來(lái)檢查當(dāng)前手機(jī)是否處于Idle狀態(tài),來(lái)執(zhí)行對(duì)應(yīng)可以執(zhí)行的Job。

服務(wù)端如何管理Job

我們知道無(wú)論是Job服務(wù),還是Alarm,或者是廣播,Service 等在系統(tǒng)中是怎么管理的,那么我們首先是需要找準(zhǔn)一條主線去一一捋清他的關(guān)系。而在JobSchedulerService 這個(gè)服務(wù)中,我們選擇的這個(gè)主線還是從設(shè)置一個(gè)Job 到觸發(fā)job 的時(shí)刻,服務(wù)端的一個(gè)流程。

該流程其實(shí)也是JobScheduler 服務(wù)的主要流程,但是這里我重點(diǎn)從job 管理這一塊去理清各個(gè)關(guān)鍵類的角色關(guān)系,以及關(guān)鍵參數(shù)的變化。

  1. 開(kāi)機(jī)會(huì)做三件事:

    1). 將8個(gè)controller 全部添加到mControllers 列表中,并開(kāi)始監(jiān)控系統(tǒng)中的狀態(tài)改變,
    2). 從/data/system/job/job.xml 中讀取持久化保存的job 并將其加入到系統(tǒng)中mJobs中。
    3). 在jobScheduler服務(wù)啟動(dòng)完畢后,發(fā)送消息MSG_CHECK_JOB 開(kāi)始檢查

  2. 從App 端設(shè)置調(diào)用jobscheduler.scheduler(job) 服務(wù),當(dāng)服務(wù)滿足各種條件之后,在startTrackingJobLocked 方法中開(kāi)始對(duì)該Job 進(jìn)行跟蹤tracking , 并將其分配對(duì)應(yīng)的Controller控制器。 添加到 mJobs 這個(gè)保存系統(tǒng)中所有Job 的列表,如果該Job 設(shè)置了persist ,那么將其異步寫(xiě)到文件中

  3. JobSchedulerService 中維持了6個(gè)重要變量:

    mActiveService: 維持了一個(gè)JobServiceContext 的列表 ,表示當(dāng)前正在運(yùn)行的Job 的Context,管理著job 的生命周期
    mPendingJob: 一個(gè)JobStatus 的列表,保存當(dāng)前pending掛起的job ,這些job都是一些準(zhǔn)備執(zhí)行的job 或者已經(jīng)滿足條件即將被執(zhí)行的job
    mJobPackageTracker:對(duì)一個(gè)package 包下的所有Job 的追蹤類,主要追蹤job 的運(yùn)行各種時(shí)間。
    mJobs : JobStore 的對(duì)象,JobStore 是一個(gè)單例模式類,有兩個(gè)主要作用:1. 保存著系統(tǒng)中所有進(jìn)程設(shè)置的job到變量mJobSet,
    mJobSet 是一個(gè)以UID 為key的List列表, 2. 持久化到本地/data/system/job/jobs.xml (需要setPersist(true))
    mMaybeQueueFunctor:mJobs.forEachJob 會(huì)調(diào)用的到JobStore中的 mJobSet 在forEachJob()方法,在此方法中,會(huì)針對(duì)uid對(duì)應(yīng)的Job遍歷執(zhí)行Functor的process 方法,是否有ready的job ,如果有則滿足條件符合其一種加入到mPendingJob,(針對(duì)手機(jī)處于當(dāng)前沒(méi)有Active的Job)
    mReadyQueueFuntor: 功能和mMaybeQueueFunctor 類似,當(dāng)有的ready 的job,則直接加入到mPendingJob 中。(針對(duì)手機(jī)處于當(dāng)前有Active的Job的狀態(tài)。)

  4. 在兩個(gè)QueueFuntor 中,都會(huì)通過(guò)mPakcageTracker 將對(duì)應(yīng)的Job 標(biāo)記nopending 和pending的時(shí)間。

其 服務(wù)端關(guān)系類圖大概如下:


服務(wù)端關(guān)系類圖.png

對(duì)上面圖做大概解釋:

  1. App 進(jìn)程通過(guò)Jobscheduler 調(diào)用到代理類JobschedulerImpl 中binder call 去調(diào)度一個(gè)job 開(kāi)始
  2. 便會(huì)將Job 加入到mJobs中,mJobs是JobStore的一個(gè)對(duì)象。JobStore 中還包裝了兩個(gè)變量mJobSet(用來(lái)存放所有Job的),和mJobsFile用來(lái)持久化一些persist的job
  3. JobSet 中保存有mJobs變量,在每次調(diào)度一個(gè)job 的時(shí)候,會(huì)判斷該job 是否為persist的job,按照如上圖所示結(jié)構(gòu)存儲(chǔ)
  4. 服務(wù)端還有一個(gè)變量較為關(guān)鍵是mPendingJobs 列表,為JobServiceContext 的一個(gè)Array,里面存放的都是已經(jīng)ready 的或者即將要被執(zhí)行的job
  5. JobPackageTracker 主要是追蹤一個(gè)包中job 的時(shí)間參數(shù),用以在計(jì)算該job 優(yōu)先級(jí)的時(shí)候,會(huì)以該job 的活躍時(shí)間占整個(gè)rebatch (30min)時(shí)間段的時(shí)間比重來(lái)重新分配優(yōu)先級(jí)
  6. 保存job 中所有詳細(xì)信息,包含:jobinfo ,job限制條件,job 的當(dāng)前狀態(tài)已滿足條件。 7. JobPackageTracker 中有個(gè)DataSet 的數(shù)據(jù)結(jié)構(gòu)。如圖所示

Controller——條件控制器

JobScheduler 服務(wù)這里總共有8個(gè)Controller ,比較特殊的有:AppIdleController.java 和 DeviceIdleJobsController.java,這兩個(gè)controller 和其他6個(gè)不一樣,他們是只要設(shè)置了job ,便都受這兩個(gè)Controller 的控制。這個(gè)后面會(huì)詳細(xì)說(shuō)明。大部分都是類似的,以其中一個(gè)Controller 為例

TimeController.java,來(lái)理解其他的Controller

當(dāng)設(shè)置一個(gè)Job 的時(shí)候,會(huì)循環(huán)所有的Controller 來(lái)為其指定限制該Job 的Controller

@Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
     if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
            maybeStopTrackingJobLocked(job, null, false);
             …………
             it.add(job);
            job.setTrackingController(JobStatus.TRACKING_TIME); // 開(kāi)始tracking 時(shí)間控制器
            maybeUpdateAlarmsLocked(
                    job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
                    job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
                    deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));  // 設(shè)置delay 或者dead line 的alarm的時(shí)間
        }
}
  
  
     // 設(shè)置delay 和deadline 的的一些alarm
    private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) {
        alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
        mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
        updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener,
                mNextDelayExpiredElapsedMillis, ws);
    }
 
    private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) {
        alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
        mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
        updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener,
                mNextJobExpiredElapsedMillis, ws);
    }
 private void checkExpiredDelaysAndResetAlarm() {
     …………
            if (ready) {
                mStateChangedListener.onControllerStateChanged();
            }
            setDelayExpiredAlarmLocked(nextDelayTime,
                    deriveWorkSource(nextDelayUid, nextDelayPackageName));
        }
    }

TimeController 的大概流程如下:

  1. JobSchedulerService 在循環(huán)設(shè)置controller 的時(shí)候,會(huì)先調(diào)用到每個(gè)controller 的aybeStartTrackingJobLocked()方法
  2. 判斷該job 是否有延遲和戒指時(shí)間限制。如果沒(méi)有,那么不為此job 設(shè)置該controller控制器
  3. 如果滿足時(shí)間限制,那么將該job 經(jīng)過(guò)一些合理性判斷后,加入到mTrackedJobs表中(滿足延遲? 延遲時(shí)間:無(wú)窮大時(shí)間; 滿足截止?截止時(shí)間:無(wú)窮大時(shí)間)
  4. 調(diào)用到 maybeUpdateAlarmsLocked 中,判斷當(dāng)前job最早觸發(fā)時(shí)間和,最遲觸發(fā)時(shí)間是否滿足條件。如果滿足,則設(shè)置其對(duì)用的Alarm,并指定對(duì)應(yīng)的Listener
  5. 當(dāng)Alarm 觸發(fā)時(shí)間到了,則會(huì)調(diào)用到對(duì)應(yīng)方法中,檢查當(dāng)前mTrackedJobs中是否有滿足條件的
  6. 如果滿足delay 時(shí)間的,則通知JobSchedulerService中去且檢查job 是否應(yīng)該觸發(fā),如果滿足deadline的,則去直接run該job


    TimeController.png

除了TimeoController 與我們聯(lián)系比較多,還有兩個(gè)controller 非常特殊,那就是DeviceIdleJobController & AppIdleController 。他們的特殊主要有三點(diǎn):

  1. 所有的Job 都必須受到這兩個(gè)controller 的控制

  2. DeviceIdleJobController 監(jiān)控設(shè)備是否進(jìn)入Doze 模式,如果進(jìn)入doze 模式,則所有的job都不能被執(zhí)行,即使正在被執(zhí)行的job 也會(huì)停掉

  3. AppIdleController 控制App 狀態(tài),如果一個(gè)app 處于了Idle狀態(tài),則會(huì)從UsageStateService.reportEvent()回調(diào)到AppIdleController。然后判斷當(dāng)前App是否為idle狀態(tài),如果是則該job即使其他條件都滿足也不會(huì)被執(zhí)行。

(注: 如何為idle狀態(tài)呢:1. 非deviceAdmin app; 2. 不是當(dāng)前給網(wǎng)絡(luò)打分的app (NETWORK_SCORE_SERVICE);3.桌面上沒(méi)有綁定小部件; 4.不是開(kāi)機(jī)引導(dǎo); 5.不是idle(距離上次亮屏大于12小時(shí)且2天沒(méi)有用過(guò)該app);6.不是運(yùn)營(yíng)商app。)

Android P 關(guān)于Job的新特性

Android 9引入了新的電池管理功能App Standby Buckets。 App Standby Buckets可根據(jù)應(yīng)用程序的最近使用頻率和頻率,幫助系統(tǒng)確定應(yīng)用程序資源請(qǐng)求的優(yōu)先級(jí)。 根據(jù)應(yīng)用程序使用模式,每個(gè)應(yīng)用程序都放在五個(gè)優(yōu)先級(jí)存儲(chǔ)區(qū)之一。 系統(tǒng)會(huì)根據(jù)應(yīng)用所在的存儲(chǔ)區(qū)限制每個(gè)應(yīng)用可用的設(shè)備資源。

共有五個(gè)standby buckets:

Active: 用戶正在使用該app,包括:lunched activity, fg service, provider used by fg app, clicks app's notification
Working set:經(jīng)常運(yùn)行,但當(dāng)前并不是active狀態(tài),例如:社交app,media app等 
Frequent:經(jīng)常使用類,但是并不一定是每天使用,例如:健身 app,團(tuán)購(gòu)app,出行app等 
Rare:很少使用類,例如:訂票app,工具類app,購(gòu)物類app等 
Never:安裝了但是從未運(yùn)行的app,系統(tǒng)對(duì)該類app 做了非常嚴(yán)格的限制

android 官網(wǎng)上一張圖能說(shuō)明各個(gè)不同的bucket 對(duì)于不同的行為的限制


AndroidP.png

具體是怎么操作的呢一張簡(jiǎn)圖來(lái)說(shuō)明一下


機(jī)器學(xué)習(xí)操作流程.png

Android P 上針對(duì)UsageStatsService 服務(wù)添加了兩個(gè)接口,setAppStandbyBucket 和getAppStandbyBucket 方法

getAppStandbyBucket(): 從系統(tǒng)中獲取,各個(gè)App 所在的Bucket 。

setAppStandbyBucket: 將機(jī)器學(xué)習(xí)訓(xùn)練好的模型,預(yù)測(cè)出的App 屬于哪個(gè)bucket 設(shè)置到系統(tǒng)中。

其P 上機(jī)器學(xué)習(xí)的流程大致如下:

  1. 具有機(jī)器學(xué)習(xí)模型的App 調(diào)用方法queryEvents 獲取某一段時(shí)間內(nèi)的一些,用戶使用App的活動(dòng)數(shù)據(jù)
  2. 并且調(diào)用getAppStandbyBucket 獲取系統(tǒng)中所有App 所處的bucket(usageStatsService 根據(jù)使用頻率計(jì)算出來(lái)的結(jié)果)。
  3. 結(jié)合1,2 中數(shù)據(jù),導(dǎo)入機(jī)器學(xué)習(xí)預(yù)測(cè)框架中,預(yù)測(cè)某一時(shí)間斷,app 應(yīng)該處于哪個(gè)bucket中。
  4. 應(yīng)用處于不同的bucket 中,對(duì)于App 的NetWo(hù)rk,Alarm,Job 都有不同的限制。

那么他是怎么限制Job 的


bucket如何限制job.png
  1. mHeartbeat :在HeartbeatAlarmListener 每次觸發(fā)一次(11分觸發(fā)一次),就會(huì)將已經(jīng)觸發(fā)的job數(shù)目累加起來(lái)
  2. 每次觸發(fā)alarm 的時(shí)候會(huì)判斷mHeartbeat > appLastRan? appLastRan 是上一次該job的觸發(fā)時(shí)的心跳數(shù)目
  3. 但job 要觸發(fā)的時(shí)候,便會(huì)經(jīng)歷以上判斷。 其實(shí)是通過(guò)heartbeat 的數(shù)目來(lái)觸發(fā)的。一次觸發(fā)多個(gè)job

綜述

jobScheduler 是一個(gè)提供給App 端設(shè)定一個(gè)滿足條件執(zhí)行的任務(wù),從App 設(shè)定一個(gè)Job到服務(wù)端接受這個(gè)job ,到服務(wù)端管理這個(gè)job,到最終觸發(fā)這個(gè)job 以及重系統(tǒng)中移除,整個(gè)流程整體并不太復(fù)雜,而是細(xì)節(jié)計(jì)算上有很多點(diǎn)比較讓人煩惱,我這里大概將其流程梳理一遍:

  1. App 創(chuàng)建一個(gè)Job,并scheduler該job
  2. JobSchedulerImpl通過(guò)Binder調(diào)用,調(diào)用到服務(wù)端JobSchedulerService端的scheduleAsPackage
  3. 在添加到mJobs集合中之前先搜索系統(tǒng)中是否有相同的Job已經(jīng)存在,如果存在則先canel掉并,再將其加入到mJobs中
  4. 判斷該Job是否有對(duì)應(yīng)的限制條件為其Job 分配對(duì)應(yīng)的Controller 。
  5. Controllers 中所有的控制器要么是監(jiān)聽(tīng)廣播,要么是注冊(cè)listener 去監(jiān)聽(tīng)系統(tǒng)狀態(tài)改變,當(dāng)系統(tǒng)狀態(tài)發(fā)生改變則都會(huì)去通過(guò)回調(diào)到JobSchedulerService
  6. onStateChanged中在發(fā)送MSG_CHECK_JOB 消息,處理對(duì)應(yīng)消息,首先判斷mReportedActive 是否為true
  7. 將ready好的Job加入到mPendingsJob列表中,然后調(diào)用到assignJobsToContextsLocked 核心方法
  8. 在assignJobsToContextsLocked 完成各種計(jì)算后,將mActiveService的可以運(yùn)行的Job,調(diào)用executeRunnableJob 方法。
  9. 調(diào)用到JobServiceContext 中,開(kāi)始啟動(dòng)服務(wù)bindService。在當(dāng)服務(wù)binder上App端的JobService 服務(wù),回調(diào)回onServiceConnected()
  10. 在doServiceBoundLocked() 中調(diào)用到handleServiceBoundLocked中通過(guò)service.startJob() ,繼而通過(guò)BinderCall 回調(diào)到JobServiceEnginee中,發(fā)送消息MSG_EXECUTE_JOB到main線程中
  11. 調(diào)用到App 實(shí)現(xiàn)的JobService 中onStartjob中,執(zhí)行App JobService 的具體事物
  12. App端調(diào)用JobFinish,binder call 調(diào)用到JobServiceContext ,清理該Job的一些資源和變量,并將其從mJobStore 中的刪掉。

下面是終極方法調(diào)用流程圖:


JobScheduler觸發(fā)Job流程圖.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 前天參加閨蜜阿黃的婚禮。 好吧,不知道該不該叫閨蜜,畢竟現(xiàn)在,這詞兒已被黑成了貶義詞。 新娘和新郎著一身漢服,行的...
    f5c8375254c2閱讀 214評(píng)論 0 2
  • (一)男人無(wú)需大費(fèi)周章 現(xiàn)在很多男人一門心思撲在工作上,希望多賺點(diǎn)錢讓愛(ài)人獲得幸福,然而現(xiàn)實(shí)是相反的,這樣并沒(méi)有讓...
    披著洋皮的郎閱讀 298評(píng)論 2 1