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 Android
developer, even if you haven't used it, you must be WorkManager
familiar with it.
Since its release in 2018, it has not been widely used as an Google
officially launched architecture component . To investigate the reason, let’s take a look at the official description of it at the beginning :LiveData
ViewModel
WorkManager
WorkManager
Used 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 WorkManager
is Android
a 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, WorkManager
it still Paging
faces 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
Android
In 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 WorkManager
to 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 APP
the research and development of a video category, and the initial business demands are as follows:
APP
log 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
: Log
local 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 4
utilize 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, WorkManager
and it did not create a wheel alone, but borrowed the class Java
of 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 ArrayDeque
thread 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, TaskExecutor
just 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
, Failed
etc 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 4
thread 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
,RxJava
or .协程
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 APP
task can always be restored and rebuilt at the right time.
Considering security, WorkManager
I finally chose to use Room
the database, and designed and maintained a very complicated table Database
that 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 WorkSpecDao
in the design except for declaring the regular Insert
, , Query
, Update
etc.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 SQL
can 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 Room
persistent 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,
WorkManager
the internal methodDao
providesDelete
a 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 JobScheduler
developers 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. |
WorkManager
It also provides a similar concept. In fact, it is JobScheduler
implemented internally based on , but WorkManager
it is not just a simple proxy.
First, compatibility uses or as a complement API
when the version is insufficient :WorkManager
AlarmManager
GcmScheduler
// 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 JobScheduler
the onStartJob
wait callback runs on the main thread by default and cannot directly perform time-consuming operations. WorkManager
Internally 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 JobScheduler
is implemented by in the system service JobSchedulerService
, its own high authority can APP
still invoke and execute JobService
the corresponding tasks after being killed or restarted.
2. New keep-alive mechanism?
Android
The 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 WorkManager
it supports periodic tasks, and APP
it can guarantee execution even if it is killed or restarted; then it must be very reasonable for me to pull down the interface IM APP
every 10
second to see if there are new messages, and start some pages or notification components by the way.APP
In fact, the demand for keeping aliveWorkManager
cannot be achieved, and its essence is that it JobScheduler
cannot be achieved:
First of all, with the iteration of the version, Android
the system's management of background tasks has become more stringent. 15
Periodic tasks that are less than minutes have been forcibly adjusted to be 15
executed in minutes to avoid the impact of frequent background timed tasks on foreground applications and avoid API
illegal 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 JobSchedulerService
have made some magic changes to the peer-to-peer-for example, when the user manually APP
shuts 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, WorkManager
the 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 APP
priority 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
Android
system's task scheduling mechanism?JobScheduler
-
2.
WorkManager
What are the internal implementation details of the component initialization mechanism, persistence mechanism, etc.? -
3.
WorkManager
What is the difference between the execution of constrained tasks and non-constrained tasks in ? -
4.
WorkManager
What 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?