JetPack WorkManager source code analysis

preface

Background tasks in Android include JobScheduler, Loader, Service and other schemes  , The emergence of WorkManager is used to replace all the above Android background task schemes, providing a unified solution for background tasks and ensuring the consistency and stability of api. At the same time, Google also takes into account the impact of background tasks on battery life when developing WorkManager. WorkManager can ensure that the task will be executed, even if the user navigates away from the screen, exits the application or restarts the device.

Basic use

Inheritance is required to use WorkManager   Worker   Class, background task in doWork()   Method

doWork()   There are three types of the return value Resutl of the method:

  • Result.success(): execution succeeded.
  • Result.failure(): execution failed.
  • Result.retry(): execution failed, please try again.
class MainWorker (context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) {

    companion object { const val TAG = "MainWorker" }

    @SuppressLint("RestrictedApi")
    override fun doWork(): Result {
        Log.d(TAG, "MainWorker doWork: The background task is executed")
        return Result.Success() // doWork successfully executed the task
    }
}

After defining a task, you can use Constraints to set Constraints. For example, you can define that the background task can only be executed when the mobile phone must be connected to the Internet, must be charging, and must be idle. Finally, you must schedule the task with WorkManager to run. More advanced use can be seen here.

    fun testBackgroundWork(view: View?) {
        //Set constraints
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED) // Must be networked
            .setRequiresCharging(true) // Must be charging
            .setRequiresDeviceIdle(true) // Must be idle
            .build()

        // Request object
        val request = OneTimeWorkRequest.Builder(MainWorker::class.java)
            .setConstraints(constraints) // Request Association constraints
            .build()

        // Join queue
        WorkManager.getInstance(this).enqueue(request)
    }

Source code

1. Initialization

In the above example, the singleton object is obtained through WorkManager.getInstance, and then the background task is executed. Take a look at the source code of this method.

    public static @NonNull WorkManagerImpl getInstance(@NonNull Context context) {
        synchronized (sLock) {
            WorkManagerImpl instance = getInstance();//It is not empty here. Return directly
            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;
        }
    }

The getInstance method will be called into the WorkManagerImpl implementation class of WorkManager. By default, the first line of code here will actually be returned directly, because getInstance is not empty, and the initialization is completed through the ContentProvider when the app is started.

 <provider
      android:name="androidx.work.impl.WorkManagerInitializer"
      android:exported="false"
      android:multiprocess="true"
      android:authorities="com.derry.workmanager.workmanager-init"
      android:directBootAware="false" />
public class WorkManagerInitializer extends ContentProvider {
    @Override
    public boolean onCreate() {
        // Initialize WorkManager with the default configuration.
        WorkManager.initialize(getContext(), new Configuration.Builder().build());
        return true;
    }
}

Find apk in the compiled build folder and check its manifest file. You can find that a provider is registered. The onCreate method of this class calls the initialization method of WorkManager. We know that ContentProvider will be initialized before the Application lifecycle method onCreate, so WorkManager will be initialized at this time.

    public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
        synchronized (sLock) {
            ...
            if (sDelegatedInstance == null) {
                context = context.getApplicationContext();
                if (sDefaultInstance == null) {
                    sDefaultInstance = new WorkManagerImpl(
                            context,
                            configuration,
                            new WorkManagerTaskExecutor(configuration.getTaskExecutor()));
                }
                sDelegatedInstance = sDefaultInstance;
            }
        }
    }

In the initialize method, configuration.getTaskExecutor() returns the thread pool of a fixed thread number internally, creates WorkManagerTaskExecutor to execute the thread pool's task, and then calls WorkManagerImpl's construction method.

    public WorkManagerImpl(
            @NonNull Context context,
            @NonNull Configuration configuration,
            @NonNull TaskExecutor workTaskExecutor,
            @NonNull WorkDatabase database) {
        Context applicationContext = context.getApplicationContext();
        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);
    }

The WorkManagerImpl constructor does the following

1. The Room database is created. The Room is also a re encapsulation of sqlite. The database is used to record the information of each background task, including execution sequence, execution time and so on;

2. Create schedulers. There are three main schedulers: GreedyScheduler, SystemAlarmScheduler and SystemJobScheduler. Choose which one to use according to the system version;

3. Create a Processor, which is used to manage the execution of Schedulers and start or stop tasks;

2. Task queue method enqueue

  Enter the WorkManagerImpl.enqueue method, create a WorkContinuationImpl object and execute the enqueue method.

    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();
    }

WorkContinuationImpl.enqueue method, create EnqueueRunnable and execute it in the background thread pool

    public @NonNull Operation enqueue() {
        if (!mEnqueued) {
            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;
    }

EnqueueRunnable.run method, add the workSpec to the database and verify the task status, register the RescheduleReceiver in AndroidManifest.xml, and execute scheduling in the scheduleWorkInBackground method

    public void run() {
        try {
            if (mWorkContinuation.hasCycles()) {
                throw new IllegalStateException(
                        String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
            }
            //Add task information to database
            boolean needsScheduling = addToDatabase();
            if (needsScheduling) {
                //Enable RescheduleReceiver, only when there are workers to be scheduled.
                final Context context =
                        mWorkContinuation.getWorkManagerImpl().getApplicationContext();
                //Allows the RescheduleReceiver to be registered in AndroidManifest.xml
                PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
                scheduleWorkInBackground();
            }
            mOperation.setState(Operation.SUCCESS);
        } catch (Throwable exception) {
            mOperation.setState(new Operation.State.FAILURE(exception));
        }
    }

3.Schedulers scheduler

EnqueueRunnable.   scheduleWorkInBackground method, called Schedulers.schedule method, and passed in three objects: configuration, workdatabase and schedulers

    public void scheduleWorkInBackground() {
        WorkManagerImpl workManager = mWorkContinuation.getWorkManagerImpl();
        Schedulers.schedule(
                workManager.getConfiguration(),
                workManager.getWorkDatabase(),
                workManager.getSchedulers());
    }

Schedulers.schedule method first performs a series of database operations, mainly querying the unexecuted tasks in the database, and then scheduling each task according to conditions. Schedulers hand over the task to each Scheduler, and greedy Scheduler will handle the task first.

    public static void schedule(
            @NonNull Configuration configuration,
            @NonNull WorkDatabase workDatabase,
            List<Scheduler> schedulers) {
        List<WorkSpec> eligibleWorkSpecs;
        ...
        if (eligibleWorkSpecs != null && eligibleWorkSpecs.size() > 0) {
            WorkSpec[] eligibleWorkSpecsArray = eligibleWorkSpecs.toArray(new WorkSpec[0]);
            // Give it to the underlying scheduler for scheduling
            for (Scheduler scheduler : schedulers) {
                scheduler.schedule(eligibleWorkSpecsArray);
            }
        }
    }

The GreedyScheduler class determines whether there are constraints. If there are constraints, the task will be collected. If not, the startWork method will be called. If there are low-power constraints, a receiver named BatteryNotLowProxy will be generated in the list file. The implementation principle is to listen to the broadcast of constraint changes, then through a series of processing, and finally call the startWork method. Other constraints are similar.

    public void schedule(@NonNull WorkSpec... workSpecs) {
        ...
        for (WorkSpec workSpec : workSpecs) {
            if (workSpec.state == WorkInfo.State.ENQUEUED
                    && !workSpec.isPeriodic()
                    && workSpec.initialDelay == 0L
                    && !workSpec.isBackedOff()) {
                
                if (workSpec.hasConstraints()) {
                    ...
                    //With constraints
                    constrainedWorkSpecs.add(workSpec);
                    constrainedWorkSpecIds.add(workSpec.id);
                } else {
                    //No constraints
                    mWorkManagerImpl.startWork(workSpec.id);
                }
            }
        }
        ...
    }

The WorkManagerImpl.startWork method and the WorkTaskExecutor executed runnable. Next, enter the implementation of run() of StartWorkRunnable

    public void startWork(
            @NonNull String workSpecId,
            @Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
        mWorkTaskExecutor
                .executeOnBackgroundThread(
                        new StartWorkRunnable(this, workSpecId, runtimeExtras));
    }

  The StartWorkRunnable.run method gives the task information to the Processor, which calls startWork() to execute the task

    public void run() {
        mWorkManagerImpl.getProcessor().startWork(mWorkSpecId, mRuntimeExtras);
    }

4.Processor

processor processors can intelligently schedule and execute background tasks on demand

processor.startWork method, where the task will be wrapped into a WorkWrapper, which is also a Runnable, and then call the WorkTaskExecutor again to execute Runnnable.

    public boolean startWork(@NonNull String id,@Nullable WorkerParameters.RuntimeExtras runtimeExtras) {

        WorkerWrapper workWrapper;
        synchronized (mLock) {
            ...
            workWrapper =
                    new WorkerWrapper.Builder(
                            mAppContext,
                            mConfiguration,
                            mWorkTaskExecutor,
                            this,
                            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);
        }
        //Execute Ruunable
        mWorkTaskExecutor.getBackgroundExecutor().execute(workWrapper);
        return true;
    }

The WorkWrapper class run method gets the ListenableWorker object by reflection, that is the parent class of the Worker class, then calls the ListenableWorker.startWork method, that is, calling the startWork method of the Worker class.

    public void run() {
        mTags = mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId);
        mWorkDescription = createWorkDescription(mTags);
        runWorker();
    }

    private void runWorker() {
        ...
        //Get ListenableWorker object by reflection
        if (mWorker == null) {
            mWorker = mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
                    mAppContext,
                    mWorkSpec.workerClassName,
                    params);
        }
        if (trySetRunning()) {
            if (tryCheckForInterruptionAndResolve()) {
                return;
            }

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

                        }
                    });
    }

Worker.startWork method, here we call the doWork method implemented by ourselves, and the whole process ends.

    public abstract @NonNull Result doWork();

    public final @NonNull ListenableFuture<Result> startWork() {
        mFuture = SettableFuture.create();
        getBackgroundExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result result = doWork();
                    mFuture.set(result);
                } catch (Throwable throwable) {
                    mFuture.setException(throwable);
                }

            }
        });
        return mFuture;
    }

Summary

WorkManager is an excellent framework and easy to use. It is suitable for tasks that can be delayed. Even if the application or device is restarted, it can be guaranteed to be executed. WorkManager will select different execution strategies for different Android versions, so it is recommended to use WorkManager instead of the original scheme if possible.

Please correct the above shortcomings. Welcome to leave a message. Thank you.

Keywords: Java Android

Added by Yetalia on Wed, 08 Dec 2021 00:46:20 +0200