Android Jetpack WorkManager source code analysis

Introduction of Android WorkManager

WorkManager is responsible for managing background tasks. It is suitable for tasks that need to ensure that the system will run even if the application exits. WorkManager selects an appropriate way to run tasks based on factors such as device API level and application status. If WorkManager executes a task while the application is running, WorkManager can run your task in a new thread of the application process. If your application is not running, WorkManager chooses an appropriate way to schedule background tasks. Depending on the device API level and included dependencies, WorkManager may use JobScheduler, Firebase JobDispatcher, or AlarmManager to schedule tasks. Why did Google create the WorkManager framework after service? 1. The abuse of service leads to the continuous execution of background tasks of mobile phones, which consumes a lot of power. 2. From the perspective of developers, after Android8.0, The management of background service is more strict. The service started in the background must be the foreground service, otherwise it will cause the crash of the application. You can also lower targetSdkVersion. 3. There are also some restrictions on targetSdkVersion from Google. Ps: Starting in 2019, all apps will have to update the Target API to the latest version within one year of each new release. Official address: Official address

The use of the WorkManager

Official DEMO Official DEMO 2.1 Gradle dependency configuration

  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"
Copy the code

Define Worker class, inherit from Worker, and then duplicate doWork() method to return Result of current task. The doWork method is executed on a child thread.

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()
    }
}
Copy the code

2.3 perform a task (1) using OneTimeWorkRequest. Builder to create objects Worker, will join the WorkManager task queue. And OneTimeWorkRequest. Builder to create a single task. (2) The task is put into the WorkManager queue for execution. The Worker does not necessarily execute immediately. WorkManager will choose the appropriate time to run the Worker and balance considerations such as system load, device insertion and so on. But if we don’t specify any constraints, WorkManager runs our task immediately.

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

2.4 repeat task. (1) using PeriodicWorkRequest Builder class to create task, create a PeriodicWorkRequest object (2) and then add tasks to the WorkManager task queue, awaiting execution. (3) The minimum interval is 15 minutes.

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

 WorkManager.getInstance(this).enqueue(pRequest)

Copy the code

2.4 Task Status You can view the workinfo. State of the task by obtaining LiveData. The task can be monitored only when Activityh is active.

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

The WorkManager allows us to specify the environment in which the task will be executed, such as when the network is connected, when the battery is full, etc., and the task will only be executed if conditions are met. (1) Create and configure Constraints objects using Constraints.Builder(). You can specify Constraints on the runtime of the task. (2) When creating the Worker, call setConstraints to specify the constraints.

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

WorkManger provides the following constraints as conditions for Work execution: (1) setrequires networktype: network connection Settings (2) setRequiresBatteryNotLow: (3) setRequiresCharging: Whether to insert equipment (connected to the power supply), default false (4) setRequiresDeviceIdle: The default value is false. (5) setRequiresStorageNotLow: Check whether the available storage of the device is lower than or equal to the critical threshold. 2.6 Canceling a Task (1) Obtain the Worker ID (2) from WorkRequest() Call workManager.getInstance ().cancelWorkById(workRequest.id) to cancel the task based on the ID. WorkManager cannot cancel tasks that are already running or completed.

WorkManager.getInstance(this).cancelWorkById(request.id)
Copy the code

Tasks are grouped by assigning TAG strings to WorkRequest objects

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

The WorkManager. GetStatusesByTag () returns a list of all tasks which the tag information.

WorkManager.getInstance(this).getWorkInfosByTag("jetpack")
Copy the code

WorkManager. CancelAllWorkByTag (cancelled) with a particular tag all the tasks

 WorkManager.getInstance(this).cancelAllWorkByTag("jetpack")
Copy the code

View the State of workinfo.state for all tasks with a particular tag by getting LiveData

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

Use the advanced

3.1 Data Interaction WorkManager can pass parameters to tasks and have tasks return results. Both pass and return values are key-value pairs. (1) Use data. Builder to create a Data object and save the key value pairs of parameters. (2) call before creating WorkQuest WorkRequest. Builder. SetInputData () the Data instance

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

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

GetInputData = jetPackWork. doWork (4) After the Data object is constructed, the result of the task is returned.

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)
    }
}
Copy the code

(5) Monitor the data returned by Worker through LiveData.

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

3.2 Chain tasks

  1. WorkManager allows a work sequence with multiple tasks to execute the tasks sequentially. () creates a sequence using the workManager.beginWith () method and passes a OneTimeWorkRequest object; , which returns a WorkContinuation object. (2) Add the remaining tasks using workcontinuation.then (). Finally, call workcontinuation. enqueue() to queue the entire sequence. If any intermediate task returns result.failure (), the sequence ends. In addition, the result data of the previous task can be used as the input data of the next task to realize data transfer between tasks.
WorkManager.getInstance(this).beginWith(request).then(twoRequest).then(threeRequest).enqueue()
Copy the code
  1. You can use the workcontinuation.bine () method to join multiple chains to create more complex sequences.

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();
Copy the code

In this case, the WorkManager runs workA before worker C, and it also runs workB before worker D. After both workB and workD are complete, the WorkManager runs workE. Note: While WorkManager runs each subchain in turn, there is no guarantee that tasks in chain 1 overlap with tasks in chain 2; for example, workB may run before or after workC, or they may run at the same time. The only guarantee is that the tasks in each subchain will run sequentially. 3.3 Unique Work Sequence You can create a unique work sequence so that only one task exists in the task queue to avoid repeated tasks. Create a unique work sequence by calling beginUniqueWork(). The meanings of the parameters are as follows: 1. Name of the work sequence 2. Policy mode when the same name sequence exists 3

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

ExistingWorkPolicy provide the following strategies: (1) ExistingWorkPolicy. REPLACE: cancel the existing sequence and REPLACE them with new sequence (2) ExistingWorkPolicy. KEEP: Retain the existing sequence and ignore your new request. (3) ExistingWorkPolicy APPEND: will the new sequence, attached to the existing at the end of the existing sequence after completion of a task to run the first task of the new sequence.

Source code analysis

Let’s look at the code with three questions and comb through the source code for WorkManager 1. There are no tasks that constrain how tasks with Constraints are performed. 2. Add how tasks are triggered by Constraints.

4.1 the WorkManager class

  1. ** workManager.getInstance (this).enqueue(request)** WorkManager.getInstance(this). WorkManager is an abstract class, and the workManager. getInstance method returns a singleton object that subclasses WorkManagerImpl. (1) Singleton initializes the WorkManagerImpl object. (2) Call getInstance to return the sDelegatedInstance object. Here the sDelegatedInstance object is no longer nulLL. Let’s look at the initialization of 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;
            }

            returnsDefaultInstance; }}Copy the code
  1. By decomcompiling our APP, we found a configuration entry for the provider in the Androidmanifest.xml file. The WorkManagerInitializer class inherits from ContentProvider. The startup process of ContentProvider is described here. The ContentProvider (WorkManagerInitializer) is initialized via ActivityThread, which executes the onCreate method.
  <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:exported="false"
            android:multiprocess="true"
            android:authorities="com.jandroid.multivideo.workmanager-init"
            android:directBootAware="false" />
Copy the code

The workManager. initialize method is called from the onCreate method of WorkManagerInitializer.

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

In the WorkManager. The initialize internal by invoking WorkManagerImpl. The initialize (the context, the configuration) complete WorkManagerImpl initialization tasks. 3. The following key see WorkManagerImpl. Initializ internal do the initialization. (1) sDelegatedInstance and sDefaultInstance are not null, indicating that they have already been initialized, throw an exception. (2) Call the constructor of WorkManagerImpl to complete the initialization task. (3) the configuration. GetTaskExecutor internal returns the default thread pool ()). (4) WorkManagerTaskExecutor internally implements thread scheduling by calling SerialExecutor.

 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; }}}Copy the code
    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)));
    }
Copy the code
  1. The Initialize method does the initialization internally by calling the constructor of the WorkManagerImpl. (1) WorkDatabase creates a database wipe, internally using the Room framework. (2) createSchedulers creates a scheduler set. There are two main types of scheduler: GreedyScheduler and SystemJobScheduler (if the system version number is greater than 23). (3) Create the Processor class. The class’s role and code will be examined next.
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);
    }
Copy the code
  1. CreateSchedulers is primarily used to create a collection of schedulers. (1) createBestAvailableBackgroundScheduler created one of the most effective background scheduler. (2) Create GreedyScheduler.
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));
    }
Copy the code
  1. CreateBestAvailableBackgroundScheduler method (1) if the Android version number > = 23, returning SystemJobScheduler, main is to use the internal JobScheduler finish scheduling (2) if the phone support GCM, Then return to GcmScheduler, and the country basically says goodbye. (3) In other cases, SystemAlarmScheduler is returned and AlarmManager is used internally to realize the principle.
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;
    }
Copy the code

4.2 Adding tasks to the execution Queue Enqueue method

  1. Workmanager.getinstance returns a WorkManagerImpl instance, so let’s look at the enQueue method. Call the enQueue method of the WorkContinuationImpl instance.
 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();
    }
Copy the code
  1. Direct access to WorkContinuationImpl. Look at the enqueue method. (1) Create EnqueueRunnable inherited from Runnable (2) get WorkManagerTaskExecutor. (3) Execute the EnqueueRunnable task in the thread pool previously created in Configuration.
 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;
    }
Copy the code
  1. The EnqueueRunnable class schedules the execution of tasks in the scheduleWorkInBackground method and internally calls the Schedule method of the Schedulers class to assign tasks.
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()); }Copy the code
  1. Schedulers class. The Scheduler’s schedule is called to swap tasks. When analyzing the WorkManager initialization, we know that there are mainly scheduling classes such as GreedyScheduler. The following focuses on the analysis of this class.
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); }}}Copy the code
  1. GreedyScheduler class (1) has constraints when judging. If no, call startWork to execute the task. Some tasks are added to the collection
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); }}}Copy the code
  1. WorkManagerImpl. The startWork method or call WorkManagerTaskExecutor executeOnBackgroundThread StartWorkRunnable execution.
 public void startWork(String workSpecId, WorkerParameters.RuntimeExtras runtimeExtras) {
        mWorkTaskExecutor
                .executeOnBackgroundThread(
                        new StartWorkRunnable(this, workSpecId, runtimeExtras));
    }
Copy the code
  1. The StartWorkRunnable class getProcessor method is the Processor object that we created when we created the WorkManager. This calls the Processor’s startWork method.
  public void run() {
        mWorkManagerImpl.getProcessor().startWork(mWorkSpecId, mRuntimeExtras);
    }
Copy the code
  1. The Processor class internally builds WorkerWrapper for Work and then calls WorkManagerTaskExecutor again to perform the WorkerWrapper task.
 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;
    }
Copy the code
  1. WorkerWrapper class (1) the reflection mechanism gets the ListenableWorker object. The Worker class inherits from ListenableWorker. (2) call ListenableWorker. The startWork, actually is to call the Worker class the startWork method. (3) In the startWork method of Worker class, the doWork method will be called again, which is the doWork method we copied.
 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(newRunnable() {
                @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(); }}Copy the code

summary

(1) Worker: specify the tasks we need to perform. The WorkManager API contains an abstract Worker class, WorkManagerImpl, from which we need to inherit and perform work. (2) WorkRequest: represents a single task. A WorkRequest object specifies which Woker class should perform the task, and we can add details to the WorkRequest object, specifying the environment in which the task will run, and so on. Each WorkRequest has an automatically generated unique ID that we can use to do things like unqueue tasks or get task status. WorkRequest is an abstract class, and in code we need to use its direct subclass, OneTimeWorkRequest or PeriodicWorkRequest. (3) WorkRequest. Builder: used to create the object WorkRequest auxiliary class, also, we want to use it. A child OneTimeWorkRequest Builder and PeriodicWorkRequest. Builder. (4) Constraints: Specifies when the task is run (for example, “only when connected to the network”). We can create Constraints objects through the Constraints.Builder and pass the Constraints objects to the WorkRequest.Builder before we create the WorkRequest. (5) WorkManager: Add WorkRequest to the team and manage WorkRequest. We are passing the WorkRequest object to the WorkManager, which schedules tasks in such a way as to spread the load on system resources while complying with the constraints we specify. (6) WorkStatus: Contains information about a specific task. WorkManager provides each WorkRequest object with a () LiveData, which holds a WorkStatus object. By observing the LiveData, we can determine the current state of the task and retrieve any values returned after the task is complete. Below is a piece of implementation class diagram information.

How are tasks triggered by task Constraints

  1. Let’s look at how the conditional task is triggered. This section uses network changes as an example to analyze the scenario. By decomcompiling our APP, we found a configuration item for receiver in the androidmanifest.xml file. Action is used to monitor network changes.

        <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>
Copy the code
  1. NetworkStateProxy class
  public static class NetworkStateProxy extends ConstraintProxy {
    }
Copy the code
  1. ConstraintProxy Class In the ConstraintProxy onReceive method, startService is a SystemAlarmService, where the ACTION is 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;
    }
Copy the code
  1. The SystemAlarmService class internally calls the mDispatcher.add method
 @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; }Copy the code
  1. The SystemAlarmDispatcher class internally calls the processCommand method
 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; }Copy the code
  1. ProcessCommand method called CommandHandler onHandleIntent method
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 ifwe have processed all commands postOnMainThread( new DequeueAndCheckForCompletion(SystemAlarmDispatcher.this)); }}}}); } finally { processCommandLock.release(); }}Copy the code
  1. OnHandleIntent the Action that onHandleIntent passes in is ACTION_CONSTRAINTS_CHANGED. The handleConstraintsChanged method is then executed, and after a series of transformations within that method, 🈶️ returns to the onHandleIntent method with the ACTION set to 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)); }}}}Copy the code
  1. DelayMetCommandHandler is called to the onAllConstraintsMet method of the DelayMetCommandHandler class. The startWork method is called inside the method. The startWork method is the Processor method. It is back to the normal work workflow analyzed above.
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 {
                    // ifwe 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)); }}}Copy the code

summary

The realization principle is by listening to the broadcast of the change of various constraints, and then through layer upon layer transformation, the final processing logic is consistent with the work process of unlimited conditions.