WorkManager principle analysis and compatibility test

Preface:

  • The basic functions and features of WorkManager can be found in the official documentation or in the Chinese translation on the nuggets

  • Demo key code reference

    • Define Worker, pop up system notification and print log every time you execute a task
  @TargetApi(Build.VERSION_CODES.O)
    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        LogHelper.logD("NotificationWorker#doWork, inputData:" + inputData.getKeyValueMap().toString());

        String text = inputData.getString("text");
     
        NotificationManager notificationManager = (NotificationManager)getApplicationContext().getSystemService(
            Context.NOTIFICATION_SERVICE);
        Notification.Builder builder = new Notification.Builder(getApplicationContext());
        Notification notification = builder
            .setTicker("WorkMgrNtf")
            .setContentTitle("WorkMgrNtf")
            .setContentText(text)
            .setSmallIcon(R.drawable.common_google_signin_btn_icon_dark)
            .build();
        notificationManager.notify(NTF_ID, notification);
        return Result.SUCCESS;
		}
Copy the code
    • Generate a periodic WorkRequest for the periodic execution of the Worker
 Data inputData = new Data.Builder()
                    .putString("text"."PeriodicWorkRequest, ts:" + System.currentTimeMillis())
                    .build();
                Constraints constraint = new Constraints.Builder()
                    .setRequiredNetworkType(NetworkType.CONNECTED)
                    .build();
                long period = Math.max(5, Integer.parseInt(mEdtPeriod.getText().toString()));
                final WorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, period,
                    TimeUnit.MINUTES)
                    .setConstraints(constraint)
                    .setInputData(inputData)
                    .build();
                mPeriodRequestId = workRequest.getId();
                WorkManager.getInstance().enqueue(workRequest);
                WorkManager.getInstance().getStatusById(workRequest.getId())
                    .observeForever(new Observer<WorkStatus>() {
                        @Override
                        public void onChanged(@Nullable WorkStatus workStatus) {
                            LogHelper.logD(
                                "OnWorkStatusChanged, requestId:" + workRequest.getId() + ", status:"+ workStatus); }});Copy the code

Core functions and main process source code analysis

The WorkManager initialization

  • The business layer does not need to manually call the initialization code. During the apK build process, a ContentProvider is registered in the androidManifest, as follows:
<provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:exported="false"
            android:multiprocess="true"
            android:authorities="com.example.ali.workmgrdemo.workmanager-init"
            android:directBootAware="false" />
Copy the code
  • When the app process initializes, it automatically installs the Provider, executes the onCreate method, and executes the initialization logic of the WorkManager:
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkManagerInitializer extends ContentProvider {
    @Override
    public boolean onCreate() {
        // Initialize WorkManager with the default configuration.
        WorkManager.initialize(getContext(), new Configuration.Builder().build());
        return true; }... }Copy the code
  • The static method WorkManagerImpl# Initialize is eventually executed to instantiate the singleton
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    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()); } sDelegatedInstance = sDefaultInstance; }}}Copy the code

WorkRequest Execution process

OneTimeWorkRequest Execution process

  • First, the WorkManager#getInstance directly accessed in Demo returns a WorkManagerImpl instance, which can be delegated externally, Only with restrictt.scope.library_group added, the business layer cannot be replaced.
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public static void setDelegate(WorkManagerImpl delegate) { synchronized (sLock) { sDelegatedInstance = delegate; }}Copy the code
  • Then, the WorkManagerImpl’s main tasks are partial DB data reads and thread scheduling. The actual task operations are encapsulated in concrete Runnable, such as: StartWorkRunnable, StopWorkRunnable, and EnqueueRunnable, all of which are executed in the same backThread
  • Before task scheduling, db persistence will be written first, and determine whether any parent task is not executed (chain execution, not shown in the figure above).
  • If the execution conditions are met, all workSpecs that meet the conditions are read from the DB again
  • Select the appropriate scheduler (depending on the system version, more on that later) to execute periodic tasks, such as the SystemJobScheduler shown above, where we focus on the execution flow of one-time tasks
  • For one-off tasks, GreedyScheuler is used to execute them immediately
  • We end up generating a WorkerWrapper (implementing the Runnable interface), instantiating the Worker in backThread (our custom NotificationWorker) and calling doWork to execute the business code.

PeriodicWorkRequest Execution process

Built-in thread pool

public class WorkManagerTaskExecutor implements TaskExecutor {

    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());

    private final Executor mMainThreadExecutor = new Executor() {
        @Override
        public void execute(@NonNull Runnable command) {
            postToMainThread(command); }}; // Avoiding synthetic accessor. volatile Thread mCurrentBackgroundExecutorThread; private final ThreadFactory mBackgroundThreadFactory = newThreadFactory() {
        @Override
        public Thread newThread(@NonNull Runnable r) {
            // Delegate to the default factory, but keep track of the current thread being used.
            Thread thread = Executors.defaultThreadFactory().newThread(r);
            mCurrentBackgroundExecutorThread = thread;
            returnthread; }}; private final ExecutorService mBackgroundExecutor = Executors.newSingleThreadExecutor(mBackgroundThreadFactory); @Override public void postToMainThread(Runnable r) { mMainThreadHandler.post(r); } @Override public ExecutorgetMainThreadExecutor() {
        return mMainThreadExecutor;
    }

    @Override
    public void executeOnBackgroundThread(Runnable r) {
        mBackgroundExecutor.execute(r);
    }

    @Override
    public Executor getBackgroundExecutor() {
        return mBackgroundExecutor;
    }

    @NonNull
    @Override
    public Thread getBackgroundExecutorThread() {
        returnmCurrentBackgroundExecutorThread; }}Copy the code
  • It can be seen that mBackgroundExecutor is a single thread pool. As for why a single thread is used to execute worker tasks, the reasons can be inferred from the process of adding tasks above:
    • DB operations are involved and tasks must be performed on non-UI threads
    • Prioritize tasks
    • Avoid data inconsistency caused by DB multi-threaded read and write operations

Scheduler Task Scheduler

The Scheduler list

  • First, the task scheduler is a fixed size 2 list:
public @NonNull List<Scheduler> getSchedulers() {
        // Initialized at construction time. So no need to synchronize.
        if (mSchedulers == null) {
            mSchedulers = Arrays.asList(
                    Schedulers.createBestAvailableBackgroundScheduler(mContext, this),
                    new GreedyScheduler(mContext, this));
        }
        return mSchedulers;
    }
Copy the code
  • GreedyScheduler is resident and is mainly used to perform one-time tasks
  • In addition to GreedySchefuler resident, another Scheduler selects the most appropriate one based on the criteria (system version, whether PlayService is installed or not) :
static @NonNull Scheduler createBestAvailableBackgroundScheduler(
            @NonNull Context context,
            @NonNull WorkManagerImpl workManager) {

        Scheduler scheduler;
        boolean enableFirebaseJobService = false;
        boolean enableSystemAlarmService = false;

        if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
            scheduler = new SystemJobScheduler(context, workManager);
            setComponentEnabled(context, SystemJobService.class, true);
            Logger.debug(TAG, "Created SystemJobScheduler and enabled SystemJobService");
        } else {
            try {
                scheduler = tryCreateFirebaseJobScheduler(context);
                enableFirebaseJobService = true;
                Logger.debug(TAG, "Created FirebaseJobScheduler");
            } catch (Exception e) {
                // Also catches the exception thrown if Play Services was not found on the device.
                scheduler = new SystemAlarmScheduler(context);
                enableSystemAlarmService = true;
                Logger.debug(TAG, "Created SystemAlarmScheduler");
            }
        }

        try {
            Class firebaseJobServiceClass = Class.forName(FIREBASE_JOB_SERVICE_CLASSNAME);
            setComponentEnabled(context, firebaseJobServiceClass, enableFirebaseJobService);
        } catch (ClassNotFoundException e) {
            // Do nothing.
        }

        setComponentEnabled(context, SystemAlarmService.class, enableSystemAlarmService);

        return scheduler;
    }
Copy the code
* If apiLevel>=23, select SystemJobScheduler to implement internal task scheduling based on JobScheculer, including periodic tasks. * If apiLevel<23 * FirebaseJobService is first constructed * If PlayService is not installed, SystemAlarmScheduler will throw an exception. Internally, it implements periodic tasks based on AlarmManagerCopy the code

Periodic task Scheduler

  • SystemAlarmScheduler
    • AlarmManager is used to generate scheduled tasks. The core code is as follows:
 if(! workSpec.hasConstraints()) { Logger.debug(TAG, String.format("Setting up Alarms for %s", workSpecId));
                Alarms.setAlarm(mContext, dispatcher.getWorkManager(), workSpecId, triggerAt);
            } else {
                // Schedule an alarm irrespective of whether all constraints matched.
                Logger.debug(TAG,
                        String.format("Opportunistically setting an alarm for %s", workSpecId));
                Alarms.setAlarm(
                        mContext,
                        dispatcher.getWorkManager(),
                        workSpecId,
                        triggerAt);
Copy the code
  • SystemJobScheduler
    • Job constraints are configured
 JobInfo.Builder builder = new JobInfo.Builder(jobId, mWorkServiceComponent)
                .setRequiredNetworkType(jobInfoNetworkType)
                .setRequiresCharging(constraints.requiresCharging())
                .setRequiresDeviceIdle(constraints.requiresDeviceIdle())
                .setExtras(extras);
Copy the code
    • Set Job period parameters
if (workSpec.isPeriodic()) {
            if (Build.VERSION.SDK_INT >= 24) {
                builder.setPeriodic(workSpec.intervalDuration, workSpec.flexDuration);
            } else {
                Logger.debug(TAG,
                        "Flex duration is currently not supported before API 24. Ignoring."); builder.setPeriodic(workSpec.intervalDuration); }}Copy the code
  • FirebaseJobService
    • Job constraints are configured
private int[] getConstraints(WorkSpec workSpec) {
        Constraints constraints = workSpec.constraints;
        List<Integer> mConstraints = new ArrayList<>();

        if (Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle()) {
            mConstraints.add(Constraint.DEVICE_IDLE);
        }

        if (constraints.requiresCharging()) {
            mConstraints.add(Constraint.DEVICE_CHARGING);
        }

        if (constraints.requiresBatteryNotLow()) {
            Logger.warning(TAG,
                    "Battery Not Low is not a supported constraint "
                            + "with FirebaseJobDispatcher");
        }

        if (constraints.requiresStorageNotLow()) {
            Logger.warning(TAG, "Storage Not Low is not a supported constraint "
                    + "with FirebaseJobDispatcher");
        }

        switch (constraints.getRequiredNetworkType()) {
            case NOT_REQUIRED: {
                // Don't add a constraint. break; } case CONNECTED: { mConstraints.add(Constraint.ON_ANY_NETWORK); break; } case UNMETERED: { mConstraints.add(Constraint.ON_UNMETERED_NETWORK); break; } case NOT_ROAMING: { Logger.warning(TAG, "Not Roaming Network is not a supported constraint with " + "FirebaseJobDispatcher. Falling back to Any Network constraint."); mConstraints.add(Constraint.ON_ANY_NETWORK); break; } case METERED: { Logger.warning(TAG, "Metered Network is not a supported constraint with " + "FirebaseJobDispatcher. Falling back to Any Network constraint."); mConstraints.add(Constraint.ON_ANY_NETWORK); break; } } return toIntArray(mConstraints); }Copy the code
    • Set Job period parameters
private void setExecutionTrigger(Job.Builder builder, WorkSpec workSpec) {
        if (Build.VERSION.SDK_INT >= 24 && workSpec.constraints.hasContentUriTriggers()) {
            builder.setTrigger(createContentUriTriggers(workSpec));
        } else if (workSpec.isPeriodic()) {
            builder.setTrigger(createPeriodicTrigger(workSpec));
            builder.setRecurring(true);
        } else{ builder.setTrigger(Trigger.NOW); }}Copy the code

Worker data structure

Constraint control

Supported constraints

  • Network type constraints include:
  • Constraints related to battery status include:
    • This parameter can be executed only when the battery is being charged
    • Limit execution under low power condition
  • Store state related constraints, only one:
    • If the available storage space is low, this operation cannot be performed

Realize the principle of

  • Each constraint corresponds to a ConstraintController, as shown below:
  • Conditional constraints based on system broadcast implementation
    • Overall structure drawing

      • ConstraintController:
        • Holds all workspecs that need to be constrained
        • Holds an instance of ConstaintTracker (singleton, different constraint types correspond to different instances)
        • Implement the ConstraintListener interface, registered to ConstraintTracker to listen for changes in the constraint state
      • ConstraintTracker:
        • Used to track the status of constraints in real time, each of which implements its own derived class
        • Only when the ConstraintController receives a WorkSpec that needs to be constrained will startTracking be called to start registering for dynamic broadcasting, listening for the corresponding system state (network, power, etc.)
    • Add a network constraint, flow analysis

Compatibility test

  • The test sample
    • Testing machine: Machine type | | | machine number system version | | — — — — — — — – | — — — — — — — – | — — — — — — — – | | | A Meizu 5.1 | | | | B Google Pixel 7.0 | | | | C Google 9.0 Pixel | |
    • Test indicators:
      • Whether system notifications are supported on worker threads
      • Whether network requests are supported on worker threads
      • Whether periodic tasks can still be executed automatically after the process is forcibly killed
  • The test data
    • ABC supports system notifications and network requests
    • Execution results of periodic tasks:
      • Testing machine a.
10-11 11:28:46. 031, 3030-3129 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}
Copy the code
The task is not executed after the strong kill, and multiple tasks are executed consecutively after the APP is restarted (specifically, the interval of the first task is about 150min, and the task cycle is 15min, so a total of 10 tasks are not executed).Copy the code
10-11 14:02:59. 727, 9889-9950 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:04. 878, 9889-9953 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:09. 974, 9889-9956 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:15. 093, 9889-9959 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:20. 208, 9889-9950 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:25. 334, 9889-9953 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:30. 406, 9889-9956 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:35. 489, 9889-9959 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:40. 665, 9889-9950 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}10-11 14:03:43. 777, 9889-9953 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539228520797}
Copy the code
  • * Test machine BCopy the code
The 2018-10-10 22:08:47. 614, 15986-16054 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539180522495}The 2018-10-10 22:26:36. 969, 15986-16287 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539180522495}NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539180522495}The 2018-10-10 22:56:17. 391, 16769-16797 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539180522495}The 2018-10-10 23:10:18. 087, 17082-17104 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539180522495}The 2018-10-10 23:23:42. 589, 17082-17290 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539180522495}
Copy the code
  • * Test machine CCopy the code
The 2018-10-13 07:37:02. 576, 16995-17018 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539385621858}The 2018-10-13 07:52:02. 524, 18022-18053 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539385621858}The 2018-10-13 08:07:02. 582, 18554-18584 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539385621858}The 2018-10-13 08:22:01. 989, 18554-19170 / com. Example. Ali. Workmgrdemo D/TAG_WORK_MGR: NotificationWorker#doWork, inputData:{text=PeriodicWorkRequest, ts:1539385621858}
Copy the code
  • Conclusion the analysis
    • In the worker thread, system notification bar and network request can be generated
    • Periodic tasks are in the non-viable state of the APP process, and the smooth execution depends on the version
      • For machines with apilevEL <23 (6.0), there is no way to guarantee smooth execution of periodic tasks
      • For machines with apilevel>=23, the periodic tasks are well guaranteed to be executed smoothly
  • Compatible with risk
    • For non-native system machines with APilevel >=23, whether JobScheculer can still ensure the smooth execution of periodic tasks needs more test data to prove

Recommended scenarios and precautions

  • It is used to establish a self-polling channel and obtain polling messages periodically. It is used to push non-real-time messages and instructions