Community Says | Talking about the Design and Implementation of WorkManager: System Overview

What is community say ?

The series of reflection blogs is a seemingly "introverted" but effective way of learning. Please refer to the origin and catalog of the series here .

predicament

As a Androiddeveloper, even if you haven't used it, you must be WorkManagerfamiliar with it.

Since its release in 2018, it has not been widely used as an Googleofficially launched architecture component . To investigate the reason, let’s take a look at the official description of it at the beginning :LiveDataViewModelWorkManager

WorkManagerUsed to execute deferrable , asynchronous background tasks. It provides a reliable , schedulable background task execution environment that can handle tasks that need to run even after the application exits or the device restarts .

Quickly distilling the key points, what do we get?

WorkManager, can handle background tasks, these tasks will be executed even if the phone is restarted?

After reading the introduction, there is no wave in my heart. "Shutdown and restart will still be executed" is indeed very good, but who cares? I don't use these at all.

Its first impression is not amazing, it can even be said to be unremarkable , whether it is the dating market or the technical field, it is very fatal.

After a series of practice and research, looking back WorkManager, the author thinks WorkManageris Androida masterpiece of academic programming style in the traditional field, and it is a legacy of the sea. It provides an excellent solution for background task processing and scheduling .

Even so, WorkManagerit still Pagingfaces the same dilemma as : Difficult to promote and unknown. Simple projects are not used, and complex projects have been precipitated for several years. Other solutions have already been applied in this field, and the cost of learning and migration is too high to be unnecessary.

——To this day, in addition to a few single blog articles using Introduction and Source Code Analysis in the community , it is still difficult for us to find related series of Advanced Combat or Best Practice .

Purpose

AndroidIn this article , the author will conduct a systematic analysis and design through the background task management mechanism for .

The ultimate goal is not to allow readers WorkManagerto introduce and use forcibly, but to have a clear understanding of background task management and scheduling tools (hereinafter referred to as background task library )—even if they have never been used, one day in the future, when encountering similar business demands, they can quickly form a clear idea and plan.

basic concept

If you want to build an excellent background task library, you need to rely on continuous iteration, optimization and expansion, and finally become a flexible and complete tool.

So what functions does the background task library need to provide, and why should these functions be designed?

The first concept that catches the eye is: thread scheduling , as the name suggests, it is the cornerstone of background asynchronous tasks.

1. Thread scheduling

For example, you are responsible for APPthe research and development of a video category, and the initial business demands are as follows:

APPlog report.

The requirements are clear and clear. Obviously, log reporting is an asynchronous task in the background , and operations are performed in sub-threads IO: Loglocal reading and writing of files, and uploading to the server.

Here we introduce the concept of TaskExecutor : it is used to execute the specific logic of background tasks. Usually, we use the thread pool to manage the thread of the task, and provide thread creation , destruction , scheduling and other functions:

// androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
public class WorkManagerTaskExecutor implements TaskExecutor {
    
    

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

  // 1.可切换主线程
  private final Executor mMainThreadExecutor = new Executor() {
    
    
    @Override
    public void execute(@NonNull Runnable command) {
    
    
        mMainThreadHandler.post(command);
    }
  };

  // 2.可切换后台工作线程
  private final Executor mBackgroundExecutor = Executors.newFixedThreadPool(
                Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)),
                createDefaultThreadFactory(isTaskExecutor));
}

In order to improve readability, the sample codes in this article have been greatly simplified . Readers should try to avoid "seeing the forest for the trees" and focus on understanding the design concept.

Here we provide the most basic thread scheduling capability for the component, which facilitates the internal switching between the main thread and the background thread . Among them, we apply for a thread pool for the background thread, and set the appropriate number of threads according to the number of available processor cores (usually the maximum number of tasks executed at the same time is ), so as to fully 4utilize the performance of the device.

2. Task queue and serialization

Next, we design a task queue to ensure that new tasks can be continuously received and distributed to idle background threads in time for execution:

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E> {
    
    

  public boolean add(E e);

  public boolean offer(E e);

  public E remove();

  public E poll();
}

A very classic queue interface, WorkManagerand it did not create a wheel alone, but borrowed the class Javaof ArrayDeque. This is a very classic implementation class, which is found in many famous tool libraries (for example RxJava), it is not expanded due to space limitations, and interested readers can check the source code by themselves.

Readers can foresee that the timing of creating and adding background tasks cannot be controlled, and ArrayDequethread synchronization was not considered at the beginning of the design, so the current design will have thread safety issues .

Therefore, the enqueuing and execution of background tasks must borrow a new role to ensure serialization, just like a single thread.

The implementation is very simple, TaskExecutorjust provide a wrapper class through proxy and locking:

public class SerialExecutorImpl implements SerialExecutor {
    
    

  // 1. 任务队列
  private final ArrayDeque<Task> mTasks;
  // 2. 后台线程的Executor,即上文中数量为4的线程池
  private final Executor mBackgroundExecutor;

  // 3.锁对象
  @GuardedBy("mLock")
  private Runnable mActive;

  @Override
  public void execute(@NonNull Runnable command) {
    
    
      // 不断地入列、出列和任务执行
      synchronized (mLock) {
    
    
          mTasks.add(new Task(this, command));
          if (mActive == null) {
    
    
            if ((mActive = mTasks.poll()) != null) {
    
    
                mExecutor.execute(mActive);
            }
          }
      }
  }
}

3. Task status, type and result callback

In the next step, we list the task states we are concerned about. It is not difficult to conclude that the states we are concerned about generally include: task start (Enqueued), task execution ( ), task success ( ), task failure ( ), task cancellation ( Running) Successded, Failedetc Cancelled.:

Please note that we defined the log reporting above as a one-time job , that is, it is only executed once, and it ends permanently after execution.

In fact, one-time work does not cover all business scenarios. For example, as a video APP, we need to record or report the user's playback progress on a regular basisAPP to ensure that even if the user is killed , it will continue to play at the last recorded playback position next time.

Here we introduce the concept of periodic tasks , which have only one termination state CANCELLED. This is because regular jobs never end. After each run, it is rescheduled regardless of the outcome.

Finally, we abstract the execution of background tasks into an interface. Developers implement the doWork interface to implement specific background services, such as log reporting, etc., and return specific results:

public abstract class Worker {
    
    

    // 后台任务的具体实现,`WorkerManager`内部进行了线程调度,执行在工作线程.
    @WorkerThread
    public abstract @NonNull Result doWork();
}

public abstract static class Result {
    
    

    @NonNull
    public static Result success() {
    
    
        return new Success();
    }

    @NonNull
    public static Result retry() {
    
    
        return new Retry();
    }

    @NonNull
    public static Result failure() {
    
    
        return new Failure();
    }
}

Persistence

So far, we have implemented a simplified version of the background task library based on in-memory queues. Next, we will further discuss the necessity of background task persistence .

1. Non-immediate tasks

In the first step, we need to introduce the concept of non-immediate tasks .

As Internet practitioners, readers should be familiar with similar pop-up windows:

The new version of your phone/PC has been downloaded: "Install Now", "Scheduled Install", "Remind me later"

Obviously, this is a common function. After the user chooses, the background of the application or system needs to upgrade or remind the user at a certain point in the future. If it is distinguished from the task types in the previous article, we can summarize the former as immediate tasks , while the latter can be called delayed tasks or non- immediate tasks .

The demands of non-immediate tasks will increase the complexity of our existing background task library exponentially , but this is necessary for the following reasons:

First of all, the periodic tasks mentioned above also belong to the category of non-immediate tasks. Although the task is executed immediately and waits, its real business logic is still triggered at a certain point in the future; secondly, and most importantly, as a robust background task library, the priority of supporting non-immediate tasks is much higher than that of immediate tasks .

——This seems counterintuitive. In daily development, instant tasks seem to be the mainstream, but we ignore the fact that resources are not unlimited .

At the beginning of the article, we built the basic thread scheduling capability and created a 4thread pool with a number of . However, as the complexity of the business increases, the thread pool may execute multiple tasks at the same time, which means that some late-entry or low-priority tasks will often wait for the previous tasks to complete.

Strictly speaking, at this time, all immediate tasks are transformed into non-immediate tasks, and further abstracted, all immediate tasks are non-immediate tasks .

Everything can be asynchronous , which is a classic concept of asynchronous programming. This idea is reflected in Handler, RxJavaor .协程

Is it reasonable for immediate tasks to be delayed? For background tasks, it is very reasonable. If the developer has a clear request that a certain piece of business logic must be executed immediately , then the background task library should not be used , but the code should be called directly in memory.

2. Persistent storage

When background tasks may be delayed, think about the next question, how to ensure the reliability of task execution?

The ultimate solution must be the persistence of background tasks . After being stored in local files or databases, even if the process is killed by the user or recycled by the system, the APPtask can always be restored and rebuilt at the right time.

Considering security, WorkManagerI finally chose to use Roomthe database, and designed and maintained a very complicated table Databasethat simply lists the core :WorkSpec

@Entity(indices = [Index(value = ["schedule_requested_at"]), Index(value = ["last_enqueue_time"])])
data class WorkSpec(

    // 1.任务执行的状态,ENQUEUED/RUNNING/SUCCEEDED/FAILED/CANCELLED
    @JvmField
    @ColumnInfo(name = "state")
    var state: WorkInfo.State = WorkInfo.State.ENQUEUED,

    // 2.Worker的类名,便于反射和日志打印
    @JvmField
    @ColumnInfo(name = "worker_class_name")
    var workerClassName: String,

    // 3.输入参数
    @JvmField
    @ColumnInfo(name = "input")
    var input: Data = Data.EMPTY,

    // 4.输出参数
    @JvmField
    @ColumnInfo(name = "output")
    var output: Data = Data.EMPTY,

    // 5.定时任务
    @JvmField
    @ColumnInfo(name = "initial_delay")
    var initialDelay: Long = 0,

    // 6.定期任务
    @JvmField
    @ColumnInfo(name = "interval_duration")
    var intervalDuration: Long = 0,

    // 7.约束关系
    @JvmField
    @Embedded
    var constraints: Constraints = Constraints.NONE,

    // ...
)

After designing the field, we design its operation class next WorkSpecDao:

@Dao
interface WorkSpecDao {
    
    
  // ...

  @Insert(onConflict = OnConflictStrategy.IGNORE)
  fun insertWorkSpec(workSpec: WorkSpec)

  @Query("SELECT * FROM workspec WHERE id=:id")
  fun getWorkSpec(id: String): WorkSpec?

  @Query("SELECT id FROM workspec")
  fun getAllWorkSpecIds(): List<String>

  @Query("UPDATE workspec SET state=:state WHERE id=:id")
  fun setState(state: WorkInfo.State, id: String): Int

  @Query("SELECT state FROM workspec WHERE id=:id")
  fun getState(id: String): WorkInfo.State?

  // ...
}

Careful readers will find that there are no type operations WorkSpecDaoin the design except for declaring the regular Insert, , Query, Updateetc.DELETE

After careful consideration, the reader can conclude that the design is reasonable—due to setState()the status of the updateable task, the completed or canceled work does not need to be deleted, but SQLcan be flexibly classified and obtained on demand through statements, such as:

@Dao
interface WorkSpecDao {
    
    
  // ...

  // 获取全部执行中的任务
  @Query("SELECT * FROM workspec WHERE state=RUNNING")
  fun getRunningWork(): List<WorkSpec>

  // 获取全部近期已完成的任务
  @Query(
      "SELECT * FROM workspec WHERE last_enqueue_time >= :startingAt AND state IN COMPLETED_STATES ORDER BY last_enqueue_time DESC"
  )
  fun getRecentlyCompletedWork(startingAt: Long): List<WorkSpec>

  // ...
}

This can be called an additional gain. Through Roompersistent storage, while ensuring that tasks can be executed stably, all tasks can also be backed up, thereby providing developers with more additional capabilities.

To be precise, WorkManagerthe internal method Daoprovides Deletea method, but it is not directly exposed to developers, but is used to resolve conflicts between tasks internally , which will be mentioned later.

priority management

Below we discuss the priority of tasks further.

Although it is clearly stated above that the behavior that needs to be executed immediately should not be used as a background task, but the corresponding business code block should be executed directly - it seems that the priority mechanism is not just needed.

But in fact, this mechanism is still necessary.

1. Constraints

Speaking of constraints , familiar JobSchedulerdevelopers will not be unfamiliar with this,

Restrictions concept
NetworkType Constrains the type of network required to run the job. For example Wi-Fi (UNMETERED).
BatteryNotLow If set to true, jobs will not run when the device is in "low battery mode".
RequiresCharging If set to true, then jobs can only run while the device is charging.
DeviceIdle If set to true, requires the user's device to be idle in order to run the job. This constraint is useful when running batch operations; without this constraint, batch operations can degrade the performance of other applications that are actively running on the user's device.
StorageNotLow If set to true, the job will not run when there is insufficient storage space on the user's device.

WorkManagerIt also provides a similar concept. In fact, it is JobSchedulerimplemented internally based on , but WorkManagerit is not just a simple proxy.

First, compatibility uses or as a complement APIwhen the version is insufficient :WorkManagerAlarmManagerGcmScheduler

// androidx.work.impl.Schedulers.java
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);
    } else {
    
    
        scheduler = tryCreateGcmBasedScheduler(context);
        if (scheduler == null) {
    
    
            scheduler = new SystemAlarmScheduler(context);
        }
    }
    return scheduler;
}

Secondly, readers know that JobSchedulerthe onStartJobwait callback runs on the main thread by default and cannot directly perform time-consuming operations. WorkManagerInternally Worker, thread scheduling is carried out, and the default implementation is WorkerThread:

// androidx.work.impl.background.systemjob.SystemJobService
public class SystemJobService extends JobService {
    
    

    public boolean onStartJob(@NonNull JobParameters params) {
    
    
      //...
      mWorkManagerImpl.startWork(...);
    }
}

// androidx.work.impl.WorkManagerImpl
public class WorkManagerImpl extends WorkManager {
    
    
  public void startWork(...) {
    
    
      mWorkTaskExecutor.executeOnTaskThread(new StartWorkRunnable(...));
  }
}

Since JobScheduleris implemented by in the system service JobSchedulerService, its own high authority can APPstill invoke and execute JobServicethe corresponding tasks after being killed or restarted.

2. New keep-alive mechanism?

AndroidThe official provides strong support for background operations, but most domestic manufacturers want to use it for keeping alive at the first time .

——For example, since WorkManagerit supports periodic tasks, and APPit can guarantee execution even if it is killed or restarted; then it must be very reasonable for me to pull down the interface IM APPevery 10second to see if there are new messages, and start some pages or notification components by the way.APP

image.png
In fact, the demand for keeping aliveWorkManager cannot be achieved, and its essence is that it JobSchedulercannot be achieved:

First of all, with the iteration of the version, Androidthe system's management of background tasks has become more stringent. 15Periodic tasks that are less than minutes have been forcibly adjusted to be 15executed in minutes to avoid the impact of frequent background timed tasks on foreground applications and avoid APIillegal abuse of :

WorkManager : I treat you like a brother, yet you want to sleep with me?

The second is the author’s guess. Due to user safety and other related considerations, domestic equipment manufacturers JobSchedulerServicehave made some magic changes to the peer-to-peer-for example, when the user manually APPshuts down the device, this operation intention has the highest priority, and even system services should not be restarted (except for the manufacturer’s white list, such as WeChat and ? QQ).

The combination of the two points, WorkManagerthe regular tasks of the platform are strictly restricted, which also means that it cannot meet similar requirements for keeping alive , and its "unstable" nature is one of the main reasons why it has less domestic applications.

3. Front desk service

Finally, we discuss how to schedule system resources in a standardized manner .

The most classic scenario is still the expediting of background tasks. Even with constraints, some background tasks still need to be specially expedited , such as sending short videos when users chat , processing payment or subscription processes , etc. These tasks are important to the user, perform quickly in the background, and need to start immediately.

The system is very strict on resource allocation for applications, and readers can easily understand it here .

Since the execution of tasks depends on the allocation of resources by the system, if you want to increase the priority of execution, you must increase the APPpriority of the component itself, so the implementation solution is already very obvious: use the foreground service .

worker.setExpedited(true)When you need to mark an urgent task by calling something like Worker, the internal implementation requires the developer to create an additional foreground notification to increase the priority and synchronize your background task behavior to the user.

summary

The summary is not a summary, there is more content that can be expanded, such as:

  • 1. What is the internal principle of the Androidsystem's task scheduling mechanism?JobScheduler

  • 2. WorkManagerWhat are the internal implementation details of the component initialization mechanism, persistence mechanism, etc.?

  • 3. WorkManagerWhat is the difference between the execution of constrained tasks and non-constrained tasks in ?

  • 4. WorkManagerWhat are the other highlights?

For reasons of space, the author will start another article for a more in-depth discussion on these issues, so stay tuned.


about me

Hello, I am Qingmei Xun . If you think the article is valuable to you, welcome ❤️, and welcome to follow my blog or GitHub .

If you feel that the article is still lacking, please follow me to urge me to write a better article—what if I improve someday?

Guess you like

Origin blog.csdn.net/mq2553299/article/details/131553791