Android Jetpack之WorkManager源碼分析

Android Jetpack之WorkManager源碼分析

Android WorkManager簡介

WorkManager 負責(zé)用來管理后臺任務(wù),它適用于需要保證系統(tǒng)即使應(yīng)用程序退出也會運行的任務(wù)姊舵,
WorkManager根據(jù)設(shè)備API級別和應(yīng)用程序狀態(tài)等因素選擇適當?shù)姆绞絹磉\行任務(wù)。如果WorkManager在應(yīng)用程序運行時執(zhí)行的任務(wù),WorkManager可以在應(yīng)用程序進程的新線程中運行您的任務(wù)恳守。如果您的應(yīng)用程序未運行易迹,WorkManager會選擇一種合適的方式來安排后臺任務(wù) 夭委。具體取決于設(shè)備API級別和包含的依賴項若河,WorkManager可能會使用 JobScheduler椒楣,F(xiàn)irebase JobDispatcher或AlarmManager調(diào)度任務(wù)。
為什么在有了service以后牡肉,google還有出WorkManager框架呢?
1.service的濫用導(dǎo)致手機后臺任務(wù)不斷執(zhí)行淆九,耗電量大统锤。
2.從開發(fā)者來說,Android8.0以后炭庙,Android對于后臺service管理的更加嚴格饲窿,應(yīng)用在后臺啟動的服務(wù)必須是前臺服務(wù),否則會導(dǎo)致應(yīng)用crash焕蹄。當然你也可以選擇降低targetSdkVersion逾雄。
3.針對targetSdkVersion Google也針對的出了一些限制。ps:2019 年起,每次發(fā)布新版本后鸦泳,所有應(yīng)用都必須在一年內(nèi)將 Target API 更新到最新版本银锻。
官方指導(dǎo)地址:官方指地址

WorkManager的使用

官方DEMO 官方DEMO
2.1 Gradle依賴配置

  def work = "2.1.0"
    implementation"androidx.work:work-runtime:$work"
    implementation"androidx.work:work-testing:$work"
//    implementation"androidx.work:work-firebase:$work"
    implementation"androidx.work:work-runtime-ktx:$work"

2.2 定義Worker類
自定義Worker類,繼承自Worker做鹰,然后復(fù)寫doWork() 方法击纬,返回當前任務(wù)的結(jié)果 Result。doWork方法是執(zhí)行在子線程的钾麸。

class JetpackWork(context: Context,workerParameters: WorkerParameters) : Worker(context,workerParameters){
    override fun doWork(): Result {
        Log.e("workermanager","work start:")
        Thread.sleep(2_000)
        Log.e("workermanager","do work thread msg :"+Thread.currentThread().name)
        return Result.success()
    }
}

2.3 執(zhí)行任務(wù)
(1)使用 OneTimeWorkRequest.Builder 創(chuàng)建對象Worker更振,將任務(wù)加入WorkManager隊列。并且OneTimeWorkRequest.Builder創(chuàng)建的是一個單次執(zhí)行的任務(wù)饭尝。
(2)將任務(wù)排入WorkManager隊列肯腕,等待執(zhí)行。
Worker不一定是立即執(zhí)行的。WorkManager會選擇適當?shù)臅r間運行Worker顷窒,平衡諸如系統(tǒng)負載宛逗,設(shè)備是否插入等考慮因素。但是如果我們沒有指定任何約束條件奈惑,WorkManager會立即運行我們的任務(wù)。

   var request = OneTimeWorkRequest.Builder(JetpackWork::class.java)
                    .build()
   WorkManager.getInstance(this).enqueue(request)

2.4 重復(fù)執(zhí)行任務(wù)
(1)使用PeriodicWorkRequest.Builder類創(chuàng)建循環(huán)任務(wù)睡汹,創(chuàng)建一個PeriodicWorkRequest對象
(2)然后將任務(wù)添加到WorkManager的任務(wù)隊列肴甸,等待執(zhí)行。
(3)最小時間間隔是15分鐘囚巴。

public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes.
 var pRequest = PeriodicWorkRequest.Builder(JetpackWork::class.java,1,TimeUnit.SECONDS).build()

 WorkManager.getInstance(this).enqueue(pRequest)

2.4 任務(wù)的狀態(tài)
通過獲取LiveData查看任務(wù)的狀態(tài)WorkInfo.State原在,只有當Activityh處于活躍狀態(tài)下才可以監(jiān)聽成功。

   WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this, Observer {
            Log.e("workermanager","state :"+it?.state?.name)
        })

2.5 任務(wù)約束Constraints
WorkManager 允許我們指定任務(wù)執(zhí)行的環(huán)境彤叉,比如網(wǎng)絡(luò)已連接庶柿、電量充足時等,在滿足條件的情況下任務(wù)才會執(zhí)行秽浇。
(1)使用Constraints.Builder()創(chuàng)建并配置Constraints對象浮庐,可以指定上訴任務(wù)運行時間時的約束。
(2)創(chuàng)建Worker時調(diào)用setConstraints指定約束條件柬焕。

var constraint = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .setRequiresCharging(true).build()
            
 var request = OneTimeWorkRequest.Builder(JetpackWork::class.java)
            .setConstraints(constraint)
            .build()

WorkManger提供了以下的約束作為Work執(zhí)行的條件:
(1)setRequiredNetworkType:網(wǎng)絡(luò)連接設(shè)置
(2)setRequiresBatteryNotLow:是否為低電量時運行 默認false
(3)setRequiresCharging:是否要插入設(shè)備(接入電源)审残,默認false
(4)setRequiresDeviceIdle:設(shè)備是否為空閑,默認false
(5)setRequiresStorageNotLow:設(shè)備可用存儲是否不低于臨界閾值
2.6 取消任務(wù)
(1)從WorkRequest()獲取Worker的ID
(2 調(diào)用WorkManager.getInstance().cancelWorkById(workRequest.id)根據(jù)ID取消任務(wù)斑举。
WorkManager 對于已經(jīng)正在運行或完成的任務(wù)是無法取消任務(wù)的搅轿。

WorkManager.getInstance(this).cancelWorkById(request.id)

2.7 添加TAG
通過為WorkRequest對象分配標記字符串來對任務(wù)進行分組

 var twoRequest = OneTimeWorkRequest.Builder(JetpackTwoWork::class.java)
            .setConstraints(constraint)
            .addTag("jetpack")
            .build()

WorkManager.getStatusesByTag() 返回該標記的所有任務(wù)的列表信息。

WorkManager.getInstance(this).getWorkInfosByTag("jetpack")

WorkManager.cancelAllWorkByTag() 取消具有特定標記的所有任務(wù)

 WorkManager.getInstance(this).cancelAllWorkByTag("jetpack")

通過獲取LiveData查看具有特定標記的所有任務(wù)的狀態(tài)WorkInfo.State

 WorkManager.getInstance(this).getWorkInfosByTagLiveData("jetpack").observe(this, Observer { 
            
        })

進階使用

3.1 數(shù)據(jù)交互
WorkManager可以將參數(shù)傳遞給任務(wù)富玷,并讓任務(wù)返回結(jié)果璧坟。傳遞和返回值都是鍵值對形式既穆。
(1)使用 Data.Builder創(chuàng)建 Data 對象,保存參數(shù)的鍵值對雀鹃。
(2)在創(chuàng)建WorkQuest之前調(diào)用WorkRequest.Builder.setInputData()傳遞Data的實例

 var requestData = Data.Builder().putString("jetpack", "workermanager").build()

        var request = OneTimeWorkRequest.Builder(JetpackWork::class.java)
            .setConstraints(constraint)
            .setInputData(requestData)
            .build()

(3 在JetpackWork.doWork方法中通過getInputData獲取傳遞值幻工。
(4)在構(gòu)建Data對象,跟隨著任務(wù)的結(jié)果返回褐澎。

class JetpackWork(context: Context,workerParameters: WorkerParameters) : Worker(context,workerParameters){
    override fun doWork(): Result {
        Log.e("workermanager","work start:"+inputData.getString("jetpack"))
        Thread.sleep(2_000)
        Log.e("workermanager","do work thread msg :"+Thread.currentThread().name)
        var outData = Data.Builder().putString("back","hi,jetpack").build()
        return Result.success(outData)
    }
}

(5) 通過LiveData監(jiān)聽Worker返回的數(shù)據(jù)会钝。

  WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this, Observer {
            Log.e("workermanager", "out data :" + it?.outputData?.getString("back"))

        })

3.2 鏈式任務(wù)

  1. WorkManager允許擁有多個任務(wù)的工作序列按照順序執(zhí)行任務(wù)。
    ()使用該WorkManager.beginWith() 方法創(chuàng)建一個序列 工三,并傳遞一個OneTimeWorkRequest對象;迁酸,該方法返回一個WorkContinuation對象。
    (2)使用 WorkContinuation.then()添加剩余的任務(wù)俭正。
    (3)最后調(diào)用WorkContinuation.enqueue()將整個序列排入隊列 奸鬓。
    如果中間有任何任務(wù)返回 Result.failure(),則整個序列結(jié)束掸读。并且上一個任務(wù)的結(jié)果數(shù)據(jù)可以作為下一個任務(wù)的輸入數(shù)據(jù)串远,實現(xiàn)任務(wù)之間的數(shù)據(jù)傳遞。
WorkManager.getInstance(this).beginWith(request).then(twoRequest).then(threeRequest).enqueue()
  1. 可以使用WorkContinuation.combine()方法連接多個鏈來創(chuàng)建更復(fù)雜的序列儿惫。
在這里插入圖片描述

要建立這個序列澡罚,先創(chuàng)建兩個單獨的鏈,然后將它們連接在一起成為第三個鏈:

WorkContinuation chainAC = WorkManager.getInstance()
    .beginWith(worker A)
    .then(worker C);
WorkContinuation chainBD = WorkManager.getInstance()
    .beginWith(worker B)
    .then(worker D);
WorkContinuation chainAll = WorkContinuation
    .combine(chainAC, chainBD)
    .then(worker E);
chainAll.enqueue();

在這種情況下肾请,WorkManager在worker C之前運行workA留搔,它也在workD之前運行workB, WorkB和workD都完成后铛铁,WorkManager 運行workE隔显。
注意:雖然WorkManager依次運行每個子鏈,但不能保證鏈1中的任務(wù)與 鏈2中的任務(wù)重疊饵逐,例如括眠,workB可能在workC之前或之后運行,或者它們可能同時運行倍权。唯一可以保證的是每個子鏈中的任務(wù)將按順序運行掷豺。
3.3 唯一的工作序列
我們可以創(chuàng)建一個唯一的工作序列,在任務(wù)隊列里薄声,同一個任務(wù)只存在一個当船,避免任務(wù)的重復(fù)執(zhí)行。通過調(diào)用 beginUniqueWork() 來創(chuàng)建唯一的工作序列奸柬。
參數(shù)含義:1、工作序列的名稱 2婴程、當有相同名稱序列時采取的策略方式 3廓奕、需要執(zhí)行的Worker

WorkManager.getInstance(this).beginUniqueWork("jetpack",ExistingWorkPolicy.APPEND,request).enqueue()

ExistingWorkPolicy提供以下策略:
(1)ExistingWorkPolicy.REPLACE:取消現(xiàn)有序列并將其替換為新序列
(2)ExistingWorkPolicy.KEEP:保留現(xiàn)有序列并忽略您的新請求
(3)ExistingWorkPolicy.APPEND:將新序列附加到現(xiàn)有序列,在現(xiàn)有序列的最后一個任務(wù)完成后運行新序列的第一個任務(wù)。

源碼分析

下面我們帶著三個問題來看代碼桌粉,梳理一下WorkManager的源碼
1.沒有任務(wù)約束Constraints的任務(wù)是如何執(zhí)行的蒸绩。
2.添加任務(wù)約束Constraints的任務(wù)是如何被觸發(fā)的。

4.1 WorkManager類

  1. 通過上面我們知道任務(wù)的執(zhí)行铃肯,是通過WorkManager.getInstance(this).enqueue(request)執(zhí)行的患亿。
    WorkManager是個抽象類,通過WorkManager.getInstance方法返回的是押逼,它的子類WorkManagerImpl的單例對象步藕。
    (1)單例模式初始化WorkManagerImpl對象
    (2)調(diào)用getInstance方法返回sDelegatedInstance對象。這里sDelegatedInstance對象已經(jīng)不是nulll了挑格。下面我們就來分析一下sDelegatedInstance的初始化過程咙冗。
 public static @NonNull WorkManagerImpl getInstance(@NonNull Context context) {
        synchronized (sLock) {
            WorkManagerImpl instance = getInstance();
            if (instance == null) {
                Context appContext = context.getApplicationContext();
                if (appContext instanceof Configuration.Provider) {
                    initialize(
                            appContext,
                            ((Configuration.Provider) appContext).getWorkManagerConfiguration());
                    instance = getInstance(appContext);
                } else {
                    throw new IllegalStateException("WorkManager is not initialized properly.  You "
                            + "have explicitly disabled WorkManagerInitializer in your manifest, "
                            + "have not manually called WorkManager#initialize at this point, and "
                            + "your Application does not implement Configuration.Provider.");
                }
            }
            return instance;
        }
    }

public static @Nullable WorkManagerImpl getInstance() {
        synchronized (sLock) {
            if (sDelegatedInstance != null) {
                return sDelegatedInstance;
            }

            return sDefaultInstance;
        }
    }
  1. 通過反編譯我們的APP,我們在AndroidManifest.xml文件中找到了一個provider的配置項漂彤。WorkManagerInitializer類又繼承自ContentProvider雾消,關(guān)于ContentProvider的啟動過程這里不過多介紹,在應(yīng)用的啟動時候挫望,會通過ActivityThread初始化ContentProvider(WorkManagerInitializer)立润,即執(zhí)行了onCreate方法。
  <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:exported="false"
            android:multiprocess="true"
            android:authorities="com.jandroid.multivideo.workmanager-init"
            android:directBootAware="false" />

在WorkManagerInitializer的onCreate方法中調(diào)用了WorkManager.initialize的方法進行初始化媳板。

 public boolean onCreate() {
        // Initialize WorkManager with the default configuration.
        WorkManager.initialize(getContext(), new Configuration.Builder().build());
        return true;
    }

在WorkManager.initialize內(nèi)部通過調(diào)用 WorkManagerImpl.initialize(context, configuration)完成WorkManagerImpl的初始化任務(wù)桑腮。

  1. 下面重點看一下WorkManagerImpl.initializ內(nèi)部做了那些初始化操作。
    (1)sDelegatedInstance和sDefaultInstance都不為空拷肌,說明已經(jīng)初始化過到旦,拋出異常
    (2)調(diào)用WorkManagerImpl的構(gòu)造方法完成初始化任務(wù)。
    (3)configuration.getTaskExecutor())內(nèi)部返回默認的線程池巨缘。
    (4)WorkManagerTaskExecutor內(nèi)部通過調(diào)用SerialExecutor實現(xiàn)線程的調(diào)度添忘。
 public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
        synchronized (sLock) {
            if (sDelegatedInstance != null && sDefaultInstance != null) {
                throw new IllegalStateException("WorkManager is already initialized.  Did you "
                        + "try to initialize it manually without disabling "
                        + "WorkManagerInitializer? See "
                        + "WorkManager#initialize(Context, Configuration) or the class level"
                        + "Javadoc for more information.");
            }

            if (sDelegatedInstance == null) {
                context = context.getApplicationContext();
                if (sDefaultInstance == null) {
                    sDefaultInstance = new WorkManagerImpl(
                            context,
                            configuration,
                            new WorkManagerTaskExecutor(configuration.getTaskExecutor()));
                }
                sDelegatedInstance = sDefaultInstance;
            }
        }
    }
    private @NonNull Executor createDefaultExecutor() {
        return Executors.newFixedThreadPool(
                // This value is the same as the core pool size for AsyncTask#THREAD_POOL_EXECUTOR.
                Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)));
    }
  1. initialize方法內(nèi)部通過調(diào)用WorkManagerImpl的構(gòu)造方法完成初始化任務(wù)。
    (1)WorkDatabase創(chuàng)建數(shù)據(jù)庫擦操作若锁,內(nèi)部使用的是Room框架搁骑。
    (2)createSchedulers創(chuàng)建調(diào)度者集合。這里面主要有兩種:GreedyScheduler和SystemJobScheduler(如果系統(tǒng)版本號大于23的話)又固。
    (3)創(chuàng)建Processor類仲器。接下來會分析該類的作用和代碼。
public WorkManagerImpl(
            @NonNull Context context,
            @NonNull Configuration configuration,
            @NonNull TaskExecutor workTaskExecutor,
            boolean useTestDatabase) {

        Context applicationContext = context.getApplicationContext();
        WorkDatabase database = WorkDatabase.create(
                applicationContext, configuration.getTaskExecutor(), useTestDatabase);
        Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
        List<Scheduler> schedulers = createSchedulers(applicationContext, workTaskExecutor);
        Processor processor = new Processor(
                context,
                configuration,
                workTaskExecutor,
                database,
                schedulers);
        internalInit(context, configuration, workTaskExecutor, database, schedulers, processor);
    }
  1. createSchedulers主要是為了創(chuàng)建調(diào)度者集合仰冠。
    (1)createBestAvailableBackgroundScheduler創(chuàng)建一個最有效的后臺調(diào)度者乏冀。
    (2)創(chuàng)建GreedyScheduler調(diào)度者。
public @NonNull List<Scheduler> createSchedulers(Context context, TaskExecutor taskExecutor) {
        return Arrays.asList(
                Schedulers.createBestAvailableBackgroundScheduler(context, this),
                // Specify the task executor directly here as this happens before internalInit.
                // GreedyScheduler creates ConstraintTrackers and controllers eagerly.
                new GreedyScheduler(context, taskExecutor, this));
    }
  1. createBestAvailableBackgroundScheduler方法
    (1)如果Android版本號>=23洋只,返回SystemJobScheduler辆沦,內(nèi)部主要是使用JobScheduler完成調(diào)度
    (2)如果手機支持GCM昼捍,則返回GcmScheduler調(diào)度者,國內(nèi)基本告別了肢扯。
    (3)其他情況下返回SystemAlarmScheduler妒茬,內(nèi)部使用AlarmManager實現(xiàn)原理。
static Scheduler createBestAvailableBackgroundScheduler(
            @NonNull Context context,
            @NonNull WorkManagerImpl workManager) {

        Scheduler scheduler;

        if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
            scheduler = new SystemJobScheduler(context, workManager);
            setComponentEnabled(context, SystemJobService.class, true);
            Logger.get().debug(TAG, "Created SystemJobScheduler and enabled SystemJobService");
        } else {
            scheduler = tryCreateGcmBasedScheduler(context);
            if (scheduler == null) {
                scheduler = new SystemAlarmScheduler(context);
                setComponentEnabled(context, SystemAlarmService.class, true);
                Logger.get().debug(TAG, "Created SystemAlarmScheduler");
            }
        }
        return scheduler;
    }

4.2 任務(wù)的入執(zhí)行隊列enqueue方法

  1. 通過以上分析我們知道WorkManager.getInstance返回的是WorkManagerImpl實例蔚晨,所以我們進入到enqueue方法中看看乍钻。調(diào)用WorkContinuationImpl實例的enqueue方法。
 public Operation enqueue(
            @NonNull List<? extends WorkRequest> workRequests) {

        // This error is not being propagated as part of the Operation, as we want the
        // app to crash during development. Having no workRequests is always a developer error.
        if (workRequests.isEmpty()) {
            throw new IllegalArgumentException(
                    "enqueue needs at least one WorkRequest.");
        }
        return new WorkContinuationImpl(this, workRequests).enqueue();
    }
  1. 直接進入WorkContinuationImpl.enqueue方法看看铭腕。
    (1)創(chuàng)建EnqueueRunnable繼承自Runnable
    (2)getWorkTaskExecutor獲取WorkManagerTaskExecutor對象银择。
    (3)通過之前在Configuration創(chuàng)建的線程池中執(zhí)行EnqueueRunnable任務(wù)。
 public @NonNull Operation enqueue() {
        // Only enqueue if not already enqueued.
        if (!mEnqueued) {
            // The runnable walks the hierarchy of the continuations
            // and marks them enqueued using the markEnqueued() method, parent first.
            EnqueueRunnable runnable = new EnqueueRunnable(this);
            mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
            mOperation = runnable.getOperation();
        } else {
            Logger.get().warning(TAG,
                    String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
        }
        return mOperation;
    }
  1. EnqueueRunnable類
    在scheduleWorkInBackground方法中調(diào)度任務(wù)執(zhí)行谨履,內(nèi)部調(diào)用Schedulers類的schedule方法欢摄,分配任務(wù)。
public void run() {
        try {
            if (mWorkContinuation.hasCycles()) {
                throw new IllegalStateException(
                        String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
            }
            boolean needsScheduling = addToDatabase();
            if (needsScheduling) {
                // Enable RescheduleReceiver, only when there are Worker's that need scheduling.
                final Context context =
                        mWorkContinuation.getWorkManagerImpl().getApplicationContext();
                PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
                scheduleWorkInBackground();
            }
            mOperation.setState(Operation.SUCCESS);
        } catch (Throwable exception) {
            mOperation.setState(new Operation.State.FAILURE(exception));
        }
    }

 public void scheduleWorkInBackground() {
        WorkManagerImpl workManager = mWorkContinuation.getWorkManagerImpl();
        Schedulers.schedule(
                workManager.getConfiguration(),
                workManager.getWorkDatabase(),
                workManager.getSchedulers());
    }
  1. Schedulers類笋粟。
    在調(diào)用Scheduler的schedule的掉配任務(wù)怀挠。在分析WorkManager初始化的時候,我們知道主要有GreedyScheduler等調(diào)度類害捕。下面重點分析一下該類绿淋。
public static void schedule(
            @NonNull Configuration configuration,
            @NonNull WorkDatabase workDatabase,
            List<Scheduler> schedulers) {
        if (schedulers == null || schedulers.size() == 0) {
            return;
        }

        WorkSpecDao workSpecDao = workDatabase.workSpecDao();
        List<WorkSpec> eligibleWorkSpecs;
        if (eligibleWorkSpecs != null && eligibleWorkSpecs.size() > 0) {
            WorkSpec[] eligibleWorkSpecsArray = eligibleWorkSpecs.toArray(new WorkSpec[0]);
            // Delegate to the underlying scheduler.
            for (Scheduler scheduler : schedulers) {
                scheduler.schedule(eligibleWorkSpecsArray);
            }
        }
    }
  1. GreedyScheduler類
    (1)判斷時候有約束條件。沒有則調(diào)用startWork執(zhí)行任務(wù)尝盼。有則將任務(wù)入集合
public void schedule(@NonNull WorkSpec... workSpecs) {
        registerExecutionListenerIfNeeded();

        // Keep track of the list of new WorkSpecs whose constraints need to be tracked.
        // Add them to the known list of constrained WorkSpecs and call replace() on
        // WorkConstraintsTracker. That way we only need to synchronize on the part where we
        // are updating mConstrainedWorkSpecs.
        List<WorkSpec> constrainedWorkSpecs = new ArrayList<>();
        List<String> constrainedWorkSpecIds = new ArrayList<>();
        for (WorkSpec workSpec: workSpecs) {
            if (workSpec.state == WorkInfo.State.ENQUEUED
                    && !workSpec.isPeriodic()
                    && workSpec.initialDelay == 0L
                    && !workSpec.isBackedOff()) {
                if (workSpec.hasConstraints()) {
                    // Exclude content URI triggers - we don't know how to handle them here so the
                    // background scheduler should take care of them.
                    if (Build.VERSION.SDK_INT < 24
                            || !workSpec.constraints.hasContentUriTriggers()) {
                        constrainedWorkSpecs.add(workSpec);
                        constrainedWorkSpecIds.add(workSpec.id);
                    }
                } else {
                    Logger.get().debug(TAG, String.format("Starting work for %s", workSpec.id));
                    mWorkManagerImpl.startWork(workSpec.id);
                }
            }
        }

        // onExecuted() which is called on the main thread also modifies the list of mConstrained
        // WorkSpecs. Therefore we need to lock here.
        synchronized (mLock) {
            if (!constrainedWorkSpecs.isEmpty()) {
                Logger.get().debug(TAG, String.format("Starting tracking for [%s]",
                        TextUtils.join(",", constrainedWorkSpecIds)));
                mConstrainedWorkSpecs.addAll(constrainedWorkSpecs);
                mWorkConstraintsTracker.replace(mConstrainedWorkSpecs);
            }
        }
    }
  1. WorkManagerImpl.startWork方法
    還是調(diào)用WorkManagerTaskExecutor的executeOnBackgroundThread方法執(zhí)行StartWorkRunnable吞滞。
 public void startWork(String workSpecId, WorkerParameters.RuntimeExtras runtimeExtras) {
        mWorkTaskExecutor
                .executeOnBackgroundThread(
                        new StartWorkRunnable(this, workSpecId, runtimeExtras));
    }
  1. StartWorkRunnable類
    getProcessor方法的是我們在創(chuàng)建WorkManager時創(chuàng)建的Processor對象。這里會調(diào)用Processor的startWork方法盾沫。
  public void run() {
        mWorkManagerImpl.getProcessor().startWork(mWorkSpecId, mRuntimeExtras);
    }
  1. Processor類
    內(nèi)部通過構(gòu)建Work的包裝類WorkerWrapper裁赠,然后再次調(diào)用WorkManagerTaskExecutor類執(zhí)行WorkerWrapper任務(wù)。
 public boolean startWork(String id, WorkerParameters.RuntimeExtras runtimeExtras) {
        WorkerWrapper workWrapper;
        synchronized (mLock) {
            // Work may get triggered multiple times if they have passing constraints
            // and new work with those constraints are added.
            if (mEnqueuedWorkMap.containsKey(id)) {
                Logger.get().debug(
                        TAG,
                        String.format("Work %s is already enqueued for processing", id));
                return false;
            }

            workWrapper =
                    new WorkerWrapper.Builder(
                            mAppContext,
                            mConfiguration,
                            mWorkTaskExecutor,
                            mWorkDatabase,
                            id)
                            .withSchedulers(mSchedulers)
                            .withRuntimeExtras(runtimeExtras)
                            .build();
            ListenableFuture<Boolean> future = workWrapper.getFuture();
            future.addListener(
                    new FutureListener(this, id, future),
                    mWorkTaskExecutor.getMainThreadExecutor());
            mEnqueuedWorkMap.put(id, workWrapper);
        }
        mWorkTaskExecutor.getBackgroundExecutor().execute(workWrapper);
        Logger.get().debug(TAG, String.format("%s: processing %s", getClass().getSimpleName(), id));
        return true;
    }
  1. WorkerWrapper類
    (1)反射機制獲取到ListenableWorker對象赴精。其中Worker類繼承自ListenableWorker類佩捞。
    (2)調(diào)用ListenableWorker.startWork,實際上是調(diào)用Worker類的startWork方法蕾哟。
    (3)在Worker類的startWork方法中又會調(diào)用doWork方法一忱,也就是我們復(fù)寫的doWork方法。
 public void run() {
        mTags = mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId);
        mWorkDescription = createWorkDescription(mTags);
        runWorker();
    }

    private void runWorker() {
        if (tryCheckForInterruptionAndResolve()) {
            return;
        }

        mWorkDatabase.beginTransaction();
        try {
            mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
            if (mWorkSpec == null) {
                Logger.get().error(
                        TAG,
                        String.format("Didn't find WorkSpec for id %s", mWorkSpecId));
                resolve(false);
                return;
            }

            // Do a quick check to make sure we don't need to bail out in case this work is already
            // running, finished, or is blocked.
            if (mWorkSpec.state != ENQUEUED) {
                resolveIncorrectStatus();
                mWorkDatabase.setTransactionSuccessful();
                Logger.get().debug(TAG,
                        String.format("%s is not in ENQUEUED state. Nothing more to do.",
                                mWorkSpec.workerClassName));
                return;
            }

            // Case 1:
            // Ensure that Workers that are backed off are only executed when they are supposed to.
            // GreedyScheduler can schedule WorkSpecs that have already been backed off because
            // it is holding on to snapshots of WorkSpecs. So WorkerWrapper needs to determine
            // if the ListenableWorker is actually eligible to execute at this point in time.

            // Case 2:
            // On API 23, we double scheduler Workers because JobScheduler prefers batching.
            // So is the Work is periodic, we only need to execute it once per interval.
            // Also potential bugs in the platform may cause a Job to run more than once.

            if (mWorkSpec.isPeriodic() || mWorkSpec.isBackedOff()) {
                long now = System.currentTimeMillis();
                // Allow first run of a PeriodicWorkRequest
                // to go through. This is because when periodStartTime=0;
                // calculateNextRunTime() always > now.
                // For more information refer to b/124274584
                boolean isFirstRun = mWorkSpec.periodStartTime == 0;
                if (!isFirstRun && now < mWorkSpec.calculateNextRunTime()) {
                    Logger.get().debug(TAG,
                            String.format(
                                    "Delaying execution for %s because it is being executed "
                                            + "before schedule.",
                                    mWorkSpec.workerClassName));
                    // For AlarmManager implementation we need to reschedule this kind  of Work.
                    // This is not a problem for JobScheduler because we will only reschedule
                    // work if JobScheduler is unaware of a jobId.
                    resolve(true);
                    return;
                }
            }

            // Needed for nested transactions, such as when we're in a dependent work request when
            // using a SynchronousExecutor.
            mWorkDatabase.setTransactionSuccessful();
        } finally {
            mWorkDatabase.endTransaction();
        }

        // Merge inputs.  This can be potentially expensive code, so this should not be done inside
        // a database transaction.
        Data input;
        if (mWorkSpec.isPeriodic()) {
            input = mWorkSpec.input;
        } else {
            InputMerger inputMerger = InputMerger.fromClassName(mWorkSpec.inputMergerClassName);
            if (inputMerger == null) {
                Logger.get().error(TAG, String.format("Could not create Input Merger %s",
                        mWorkSpec.inputMergerClassName));
                setFailedAndResolve();
                return;
            }
            List<Data> inputs = new ArrayList<>();
            inputs.add(mWorkSpec.input);
            inputs.addAll(mWorkSpecDao.getInputsFromPrerequisites(mWorkSpecId));
            input = inputMerger.merge(inputs);
        }

        WorkerParameters params = new WorkerParameters(
                UUID.fromString(mWorkSpecId),
                input,
                mTags,
                mRuntimeExtras,
                mWorkSpec.runAttemptCount,
                mConfiguration.getExecutor(),
                mWorkTaskExecutor,
                mConfiguration.getWorkerFactory());

        // Not always creating a worker here, as the WorkerWrapper.Builder can set a worker override
        // in test mode.
        if (mWorker == null) {
            mWorker = mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
                    mAppContext,
                    mWorkSpec.workerClassName,
                    params);
        }

        if (mWorker == null) {
            Logger.get().error(TAG,
                    String.format("Could not create Worker %s", mWorkSpec.workerClassName));
            setFailedAndResolve();
            return;
        }

        if (mWorker.isUsed()) {
            Logger.get().error(TAG,
                    String.format("Received an already-used Worker %s; WorkerFactory should return "
                            + "new instances",
                            mWorkSpec.workerClassName));
            setFailedAndResolve();
            return;
        }
        mWorker.setUsed();

        // Try to set the work to the running state.  Note that this may fail because another thread
        // may have modified the DB since we checked last at the top of this function.
        if (trySetRunning()) {
            if (tryCheckForInterruptionAndResolve()) {
                return;
            }

            final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
            // Call mWorker.startWork() on the main thread.
            mWorkTaskExecutor.getMainThreadExecutor()
                    .execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                Logger.get().debug(TAG, String.format("Starting work for %s",
                                        mWorkSpec.workerClassName));
                                mInnerFuture = mWorker.startWork();
                                future.setFuture(mInnerFuture);
                            } catch (Throwable e) {
                                future.setException(e);
                            }

                        }
                    });

            // Avoid synthetic accessors.
            final String workDescription = mWorkDescription;
            future.addListener(new Runnable() {
                @Override
                @SuppressLint("SyntheticAccessor")
                public void run() {
                    try {
                        // If the ListenableWorker returns a null result treat it as a failure.
                        ListenableWorker.Result result = future.get();
                        if (result == null) {
                            Logger.get().error(TAG, String.format(
                                    "%s returned a null result. Treating it as a failure.",
                                    mWorkSpec.workerClassName));
                        } else {
                            Logger.get().debug(TAG, String.format("%s returned a %s result.",
                                    mWorkSpec.workerClassName, result));
                            mResult = result;
                        }
                    } catch (CancellationException exception) {
                        // Cancellations need to be treated with care here because innerFuture
                        // cancellations will bubble up, and we need to gracefully handle that.
                        Logger.get().info(TAG, String.format("%s was cancelled", workDescription),
                                exception);
                    } catch (InterruptedException | ExecutionException exception) {
                        Logger.get().error(TAG,
                                String.format("%s failed because it threw an exception/error",
                                        workDescription), exception);
                    } finally {
                        onWorkFinished();
                    }
                }
            }, mWorkTaskExecutor.getBackgroundExecutor());
        } else {
            resolveIncorrectStatus();
        }
    }

小結(jié)

(1)Worker:指定我們需要執(zhí)行的任務(wù)谭确。 WorkManager API包含一個抽象的Worker類WorkManagerImpl帘营,我們需要繼承這個類并且在這里執(zhí)行工作。
(2)WorkRequest:代表一個單獨的任務(wù)逐哈。一個WorkRequest 對象指定哪個 Woker 類應(yīng)該執(zhí)行該任務(wù)芬迄,而且,我們還可以向 WorkRequest 對象添加詳細信息昂秃,指定任務(wù)運行的環(huán)境等禀梳。每個 WorkRequest 都有一個自動生成的唯一ID择诈,我們可以使用該ID來執(zhí)行諸如取消排隊的任務(wù)或獲取任務(wù)狀態(tài)等內(nèi)容。 WorkRequest 是一個抽象類出皇,在代碼中,我們需要使用它的直接子類哗戈,OneTimeWorkRequest 或 PeriodicWorkRequest.郊艘。
(3)WorkRequest.Builder:用于創(chuàng)建WorkRequest對象的輔助類,同樣唯咬,我們要使用它的一個子OneTimeWorkRequest.Builder 和PeriodicWorkRequest.Builder 纱注。
(4)Constraints:指定任務(wù)在何時運行(例如,“僅在連接到網(wǎng)絡(luò)時”)胆胰。我們可以通過Constraints.Builder 來創(chuàng)建Constraints對象狞贱,并在創(chuàng)建WorkRequest之前,將 Constraints 對象傳遞給 WorkRequest.Builder蜀涨。
(5)WorkManager:將WorkRequest入隊和管理WorkRequest瞎嬉。我們要將WorkRequest對象傳遞給 WorkManager ,WorkManager 以這樣的方式調(diào)度任務(wù)厚柳,以便分散系統(tǒng)資源的負載氧枣,同時遵守我們指定的約束條件。
(6)WorkStatus:包含有關(guān)特定任務(wù)的信息别垮。WorkManager 為每個 WorkRequest 對象提供一個()LiveData便监,LiveData持有一個WorkStatus對象,通過觀察LiveData碳想,我們可以確定任務(wù)的當前狀態(tài)烧董,并在任務(wù)完成后獲取返回的任何值。
下面以貼一張執(zhí)行的類圖信息胧奔。


image

任務(wù)約束Constraints的任務(wù)是如何被觸發(fā)

  1. 結(jié)下來分析一下如果帶條件約束的任務(wù)是如何被觸發(fā)的逊移。以網(wǎng)絡(luò)變化為例分析該場景。
    通過反編譯我們的APP葡盗,我們在AndroidManifest.xml文件中找到了一個receiver的配置項螟左。
    通過action,我們知道主要是為了監(jiān)聽網(wǎng)絡(luò)的變化的觅够。

        <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
            android:enabled="false"
            android:exported="false"
            android:directBootAware="false">

            <intent-filter>

                <action
                    android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            </intent-filter>
        </receiver>
  1. NetworkStateProxy類
  public static class NetworkStateProxy extends ConstraintProxy {
    }
  1. ConstraintProxy類
    在ConstraintProxy類的onReceive方法中胶背,startService一個SystemAlarmService,其中ACTION
    為ACTION_CONSTRAINTS_CHANGED喘先。
 @Override
    public void onReceive(Context context, Intent intent) {
        Logger.get().debug(TAG, String.format("onReceive : %s", intent));
        Intent constraintChangedIntent = CommandHandler.createConstraintsChangedIntent(context);
        context.startService(constraintChangedIntent);
    }

  static Intent createConstraintsChangedIntent(@NonNull Context context) {
        Intent intent = new Intent(context, SystemAlarmService.class);
        intent.setAction(ACTION_CONSTRAINTS_CHANGED);
        return intent;
    }
  1. SystemAlarmService類
    內(nèi)部調(diào)用mDispatcher.add方法
 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        if (mIsShutdown) {
            Logger.get().info(TAG,
                    "Re-initializing SystemAlarmDispatcher after a request to shut-down.");

            // Destroy the old dispatcher to complete it's lifecycle.
            mDispatcher.onDestroy();
            // Create a new dispatcher to setup a new lifecycle.
            initializeDispatcher();
            // Set mIsShutdown to false, to correctly accept new commands.
            mIsShutdown = false;
        }

        if (intent != null) {
            mDispatcher.add(intent, startId);
        }

        // If the service were to crash, we want all unacknowledged Intents to get redelivered.
        return Service.START_REDELIVER_INTENT;
    }
  1. SystemAlarmDispatcher類
    內(nèi)部調(diào)用processCommand方法
 public boolean add(@NonNull final Intent intent, final int startId) {
        Logger.get().debug(TAG, String.format("Adding command %s (%s)", intent, startId));
        assertMainThread();
        String action = intent.getAction();
        if (TextUtils.isEmpty(action)) {
            Logger.get().warning(TAG, "Unknown command. Ignoring");
            return false;
        }

        // If we have a constraints changed intent in the queue don't add a second one. We are
        // treating this intent as special because every time a worker with constraints is complete
        // it kicks off an update for constraint proxies.
        if (CommandHandler.ACTION_CONSTRAINTS_CHANGED.equals(action)
                && hasIntentWithAction(CommandHandler.ACTION_CONSTRAINTS_CHANGED)) {
            return false;
        }

        intent.putExtra(KEY_START_ID, startId);
        synchronized (mIntents) {
            boolean hasCommands = !mIntents.isEmpty();
            mIntents.add(intent);
            if (!hasCommands) {
                // Only call processCommand if this is the first command.
                // The call to dequeueAndCheckForCompletion will process the remaining commands
                // in the order that they were added.
                processCommand();
            }
        }
        return true;
    }

  1. processCommand方法
    內(nèi)部調(diào)用CommandHandler.onHandleIntent方法
private void processCommand() {
        assertMainThread();
        PowerManager.WakeLock processCommandLock =
                WakeLocks.newWakeLock(mContext, PROCESS_COMMAND_TAG);
        try {
            processCommandLock.acquire();
            // Process commands on the background thread.
            mWorkManager.getWorkTaskExecutor().executeOnBackgroundThread(new Runnable() {
                @Override
                public void run() {
                    synchronized (mIntents) {
                        mCurrentIntent = mIntents.get(0);
                    }

                    if (mCurrentIntent != null) {
                        final String action = mCurrentIntent.getAction();
                        final int startId = mCurrentIntent.getIntExtra(KEY_START_ID,
                                DEFAULT_START_ID);
                        Logger.get().debug(TAG,
                                String.format("Processing command %s, %s", mCurrentIntent,
                                        startId));
                        final PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(
                                mContext,
                                String.format("%s (%s)", action, startId));
                        try {
                            Logger.get().debug(TAG, String.format(
                                    "Acquiring operation wake lock (%s) %s",
                                    action,
                                    wakeLock));

                            wakeLock.acquire();
                            mCommandHandler.onHandleIntent(mCurrentIntent, startId,
                                    SystemAlarmDispatcher.this);
                        } catch (Throwable throwable) {
                            Logger.get().error(
                                    TAG,
                                    "Unexpected error in onHandleIntent",
                                    throwable);
                        }  finally {
                            Logger.get().debug(
                                    TAG,
                                    String.format(
                                            "Releasing operation wake lock (%s) %s",
                                            action,
                                            wakeLock));
                            wakeLock.release();
                            // Check if we have processed all commands
                            postOnMainThread(
                                    new DequeueAndCheckForCompletion(SystemAlarmDispatcher.this));
                        }
                    }
                }
            });
        } finally {
            processCommandLock.release();
        }
    }

  1. onHandleIntent方法
    onHandleIntent傳入的Action 是ACTION_CONSTRAINTS_CHANGED钳吟。然后執(zhí)行handleConstraintsChanged方法,在該方法內(nèi)部經(jīng)過一系列轉(zhuǎn)化窘拯,???會回到onHandleIntent方法中红且,而且ACTION為ACTION_DELAY_MET坝茎。
 void onHandleIntent(
            @NonNull Intent intent,
            int startId,
            @NonNull SystemAlarmDispatcher dispatcher) {

        String action = intent.getAction();

        if (ACTION_CONSTRAINTS_CHANGED.equals(action)) {
            handleConstraintsChanged(intent, startId, dispatcher);
        } else if (ACTION_RESCHEDULE.equals(action)) {
            handleReschedule(intent, startId, dispatcher);
        } else {
            Bundle extras = intent.getExtras();
            if (!hasKeys(extras, KEY_WORKSPEC_ID)) {
                Logger.get().error(TAG,
                        String.format("Invalid request for %s, requires %s.",
                                action,
                                KEY_WORKSPEC_ID));
            } else {
                if (ACTION_SCHEDULE_WORK.equals(action)) {
                    handleScheduleWorkIntent(intent, startId, dispatcher);
                } else if (ACTION_DELAY_MET.equals(action)) {
                    handleDelayMet(intent, startId, dispatcher);
                } else if (ACTION_STOP_WORK.equals(action)) {
                    handleStopWork(intent, startId, dispatcher);
                } else if (ACTION_EXECUTION_COMPLETED.equals(action)) {
                    handleExecutionCompleted(intent, startId, dispatcher);
                } else {
                    Logger.get().warning(TAG, String.format("Ignoring intent %s", intent));
                }
            }
        }
    }
  1. DelayMetCommandHandler
    經(jīng)過成成調(diào)用,會調(diào)用到DelayMetCommandHandler類onAllConstraintsMet方法暇番。在該方法內(nèi)部會調(diào)用startWork方法嗤放。而startWork方法正式Processor的方法。又回到了上面分析的正常work的工作流程了壁酬。
  public void onAllConstraintsMet(@NonNull List<String> workSpecIds) {
        // WorkConstraintsTracker will call onAllConstraintsMet with list of workSpecs whose
        // constraints are met. Ensure the workSpecId we are interested is part of the list
        // before we call Processor#startWork().
        if (!workSpecIds.contains(mWorkSpecId)) {
            return;
        }

        synchronized (mLock) {
            if (mCurrentState == STATE_INITIAL) {
                mCurrentState = STATE_START_REQUESTED;

                Logger.get().debug(TAG, String.format("onAllConstraintsMet for %s", mWorkSpecId));
                // Constraints met, schedule execution
                // Not using WorkManagerImpl#startWork() here because we need to know if the
                // processor actually enqueued the work here.
                boolean isEnqueued = mDispatcher.getProcessor().startWork(mWorkSpecId);

                if (isEnqueued) {
                    // setup timers to enforce quotas on workers that have
                    // been enqueued
                    mDispatcher.getWorkTimer()
                            .startTimer(mWorkSpecId, WORK_PROCESSING_TIME_IN_MS, this);
                } else {
                    // if we did not actually enqueue the work, it was enqueued before
                    // cleanUp and pretend this never happened.
                    cleanUp();
                }
            } else {
                Logger.get().debug(TAG, String.format("Already started work for %s", mWorkSpecId));
            }
        }
    }

小結(jié)

實現(xiàn)原理就是通過監(jiān)聽各種約束條件變化的廣播次酌,然后經(jīng)過層層轉(zhuǎn)化,最終的處理邏輯和無限制條件的work流程一致舆乔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岳服,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子希俩,更是在濱河造成了極大的恐慌吊宋,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颜武,死亡現(xiàn)場離奇詭異璃搜,居然都是意外死亡,警方通過查閱死者的電腦和手機鳞上,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門腺劣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人因块,你說我怎么就攤上這事橘原。” “怎么了涡上?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵趾断,是天一觀的道長。 經(jīng)常有香客問我吩愧,道長芋酌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任雁佳,我火速辦了婚禮脐帝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糖权。我一直安慰自己,他們只是感情好疚顷,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布阀坏。 她就那樣靜靜地躺著,像睡著了一般浸船。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天黔州,我揣著相機與錄音牲蜀,去河邊找鬼。 笑死,一個胖子當著我的面吹牛寇窑,可吹牛的內(nèi)容都是我干的先慷。 我是一名探鬼主播缎浇,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼指厌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了牡借?” 一聲冷哼從身側(cè)響起御铃,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤根竿,失蹤者是張志新(化名)和其女友劉穎嗓违,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡溯饵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年隘谣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡码泛,死狀恐怖枚钓,靈堂內(nèi)的尸體忽然破棺而出氢烘,到底是詐尸還是另有隱情家厌,我是刑警寧澤播玖,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站掰吕,受9級特大地震影響果覆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜殖熟,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一局待、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦钳榨、人聲如沸舰罚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沸停。三九已至,卻和暖如春昭卓,著一層夾襖步出監(jiān)牢的瞬間愤钾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工候醒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留能颁,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓倒淫,卻偏偏與公主長得像伙菊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子敌土,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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