轻松管理你的后台任务—WorkManager(进阶篇)
zilicheOwns关注赞赏支持轻松管理你的后台任务—WorkManager(进阶篇)
0.112 字数 2063阅读 32预备知识
1.Android 基础知识
2.LiveData基本了解和使用
3.Room数据库的基本了解和使用
4.WorkManager的基本了解和使用
读完本文你可以达到什么程度
- 如何根据业务选择合适的ThreadPoolExecutor
- WorkManager的启动、任务执行的流程
一、如何根据业务选择合适的ThreadPoolExecutor
线程池是一种多线程处理形式,处理过程中将任务添加到队列中,后面再创建线程去处理这些任务,线程池里面的线程都是后台线程,每个线程都是默认的优先级。如果某个线程处于空闲中,将添加一个任务进来,让空闲线程去处理任务。如果所有线程都很繁忙,消息队列会挂起,等待某个线程池空闲后再处理任务。
WorkManager执行任务离不开线程池,我特别喜欢研究某个框架的线程池,从某个点能够折射出框架是否优秀。所以我先看了一下WorkManager的线程池定义。
// Avoiding synthetic accessor.
volatile Thread mCurrentBackgroundExecutorThread;
private final ThreadFactory mBackgroundThreadFactory = new ThreadFactory() {
private int mThreadsCreated = 0;
@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);
thread.setName("WorkManager-WorkManagerTaskExecutor-thread-" + mThreadsCreated);
mThreadsCreated++;
mCurrentBackgroundExecutorThread = thread;
return thread;
}
};
private final ExecutorService mBackgroundExecutor =
Executors.newSingleThreadExecutor(mBackgroundThreadFactory);
一开始在想WorkManager如何定义线程池的时候,我自己先想了一下,首先我想的是任务需要顺序执行,想要顺序执行大家肯定想到的是Executors. newSingleThreadExecutor。这个一个核心线程数和最大线程数都是1的单线程线程池,当执行任务入队,这个线程就会执行任务,其他的任务等待执行。 想要顺序执行,我想起了AsyncTask的做法,个人觉得AsyncTask的写法还是比较巧妙的。
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
SerialExecutor 创建了一个ArrayDeque双端队列,这里当成队列来使用,执行任务的时候将Runnable添加到mTasks中,一开始mActive是没有值的,所以执行scheduleNext,然后mTasks出队,取出Runnable赋值给mActive,如果不为空,则执行Runnable,等待r.run()执行完成再取下一个Runnable执行。这样就完成任务的顺序执行。
所以看到上面给出的代码也就证明了我的想法,WorkManager也是简单粗暴。对于不同的需求对于线程池的设计是不一样的,一个设计的优秀的线程池能够减少线程过少带来的CPU闲置与线程过多给JVM内存与线程切换时系统调用的压力。
这里我举个Okhtttp的线程池的设计案例来证明选择是如此重要。我们知道okhttp很优秀,很多巧妙的设计都是值得我们学习的。
private int maxRequests = 64;
/** Executes calls. Created lazily. */
private ExecutorService executorService;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
核心线程数量,保持在线程池中的线程数量(即使已经空闲),为0代表线程空闲后不会保留,等待一段时间后停止。最大容纳线程数是 Integer.MAX_VALUE。TimeUnit.SECOND是当线程池中的线程数量大于核心线程时,空闲的线程就会等待60s才会被终止,如果小于,则会立刻停止。 new SynchronousQueue<Runnable>():线程等待队列。同步队列,按序排队。
SynchronousQueue每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此队列内部其实没有任何一个元素,或者说容量为0,严格说并不是一种容器,由于队列没有容量,因此不能调用peek等操作,因此只有移除元素才有元素,显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入者(生产者)传递给移除者(消费者),这在多任务队列中最快的处理任务方式。
个人认为这样是非常巧妙的,我们当然是希望okhttp是高并发,同时可以发送多个请求,因此网络请求是个高频操作,这样是比较合适的,最大容纳线程数是 Integer.MAX_VALUE,因此理论上可以同时发这么多请求,但是优秀的okhttp肯定不会让你这么干的,它设计两个ArrayDeque,一个正在running的队列,一个是待执行的队列,执行任务完成的时候都会promote一下,问一下当前正在running的队列有没有满,没有满则从待执行的队列中去取放到running队列,如果满了,就不加了。
小结一下 设计好线程池对一个框架是多么的重要。
二、WorkManager的启动、任务执行的流程
依然是上篇文章的那张图。
WorkManager流程图
2.1 WorkManager的启动
我们观察到app一启动的时候时候看日志WorkManager就已经开始工作了,这个引起了我的好奇心,我通过WorkManger.getInstance()一路反推过去找到了初始化的地方。看一下链路。
WorkManger.getInstance() -> WorkManagerImpl.getInstance() -> WorkManagerImpl.initialize -> WorkManagerInitializer.onCreate
这是反推过去的,大家看反过来看。看到下面的代码估计大家就瞬间明白了,WorkManagerInitializer 继承了ContentProvider,
public class WorkManagerInitializer extends ContentProvider {
@Override
public boolean onCreate() {
// Initialize WorkManager with the default configuration.
WorkManager.initialize(getContext(), new Configuration.Builder().build());
return true;
}
}
ContentProvider是什么时候初始化的,我们可以去看看Android源码,App启动会创建ActivityThread。在主线程中初始化ContentProvider。看一下调用链路。
ActivityThread#installContentProviders() -> ActivityThread#installProvider() -> ContentProvider#attachInfo() -> ContentProvider.this.onCreate()
但是对于一个大型的App来说,这好事也变成坏事了,为了控制App的启动流程,我们做了大量的监控分析,WorkManager肯定是通不过的,所以我们必须把启动初始化给干掉,遵守哪里需要哪里出来话的规则。
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
我们可以自己自定义配置。
// provide custom configuration
Configuration myConfig = new Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build();
//initialize WorkManager
WorkManager.initialize(this, myConfig);
2.2 WorkManager初始化
public WorkManagerImpl(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
boolean useTestDatabase) {
Context applicationContext = context.getApplicationContext();
WorkDatabase database = WorkDatabase.create(applicationContext, useTestDatabase);
Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
List<Scheduler> schedulers = createSchedulers(applicationContext);
Processor processor = new Processor(
context,
configuration,
workTaskExecutor,
database,
schedulers);
internalInit(context, configuration, workTaskExecutor, database, schedulers, processor);
}
- 创建好数据库,WorkManager正式版数据库操作都在子线程中操作,如果你想在主线程操作,那么需要allowMainThreadQueries()。
if (useTestDatabase) {
builder = Room.inMemoryDatabaseBuilder(context, WorkDatabase.class)
.allowMainThreadQueries();
} else {
builder = Room.databaseBuilder(context, WorkDatabase.class, DB_NAME);
}
- 创建Scheduler,根据版本返回一个List<Scheduler>, 里面包含两个GreedyScheduler,SystemJobScheduler/SystemAlarmScheduler。GreedyScheduler执行任务不强制执行,如果遭遇系统低杀或者app异常任务就会中断执行。SystemAlarmScheduler执行任务中会持有WakeLock,保证任务的执行。
Arrays.asList(
Schedulers.createBestAvailableBackgroundScheduler(context, this),
new GreedyScheduler(context, this));
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 = new SystemAlarmScheduler(context);
enableSystemAlarmService = true;
Logger.get().debug(TAG, "Created SystemAlarmScheduler");
}
- 创建Processor,控制中心,Scheduler最后都会殊途同归,调用Processor.startWork(),去执行业务方写的Worker中的逻辑。
- internalInit,首次app启动去检查数据库中需要重新安排执行的任务,判断满足条件后去执行。
2.3 ListenableWorker
WorkManager执行任务完成时我们必须要监听,因为需要去更新数据库的任务状态。WorkManager通过提供ListenableWorker支持这种案例。ListenableWorker是最低级别的worker
public abstract class Worker extends ListenableWorker {
// Package-private to avoid synthetic accessor.
SettableFuture<Result> mFuture;
@Keep
@SuppressLint("BanKeepAnnotation")
public Worker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
/**
* Override this method to do your actual background processing. This method is called on a
* background thread - you are required to <b>synchronously</b> do your work and return the
* {@link androidx.work.ListenableWorker.Result} from this method. Once you return from this
* method, the Worker is considered to have finished what its doing and will be destroyed. If
* you need to do your work asynchronously on a thread of your own choice, see
* {@link ListenableWorker}.
* <p>
* A Worker is given a maximum of ten minutes to finish its execution and return a
* {@link androidx.work.ListenableWorker.Result}. After this time has expired, the Worker will
* be signalled to stop.
*
* @return The {@link androidx.work.ListenableWorker.Result} of the computation; note that
* dependent work will not execute if you use
* {@link androidx.work.ListenableWorker.Result#failure()} or
* {@link androidx.work.ListenableWorker.Result#failure(Data)}
*/
@WorkerThread
public abstract @NonNull Result doWork();
@Override
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;
}
}
抽象方法ListenableWorker.startWork() 返回一个Result的ListenableFuture。ListenableFuture是一个轻量级的接口;它是一个提供附着监听并且处理异常的Future。当后台操作完成时该方法返回一个带有结果的ListenableFuture。
2.4 Room的Transaction操作
你在使用的时候你会发现代码是这样写的。
//入队
WorkManager.getInstance().beginWith(uploadWorkRequest).enqueue();
//监听
WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorkRequest.getStringId()).observe(this, new Observer<WorkInfo>() {
@Override
public void onChanged(@Nullable WorkInfo workInfo) {
}
});
任务入队,WorkManager需要先将任务存储到数据库,存储是需要在子线程中执行的,但是执行到getWorkInfoByIdLiveData的时候,要立马去查询数据库返回一个LiveData对象。所以大家不会觉得奇怪吗,可能入队还没执行结束,下面的代码可以就先执行了,这样不是取不到数据吗?因此我怀着好奇的心去一探究竟。
/**
* get state of {@link WorkSpec} by liveData.
*
* @param id {@link WorkSpec}'s id to listen the workState.
* @return {@link LiveData}, {@link WorkInfo}
*/
@Override
public LiveData<WorkInfo> getWorkInfoByIdLiveData(@NonNull String id) {
WorkSpecDao dao = getWorkDatabase().workSpecDao();
LiveData<WorkSpec> inputLiveData = dao.getWorkSpec(id);
LiveData<WorkInfo> deduped = LiveDataUtils
.dedupedMappedLiveDataFor(inputLiveData, input -> {
WorkInfo workInfo = null;
if (input != null) {
workInfo = new WorkInfo(input.id,
input.state, input.input, input.output);
}
return workInfo;
});
return mLiveDataTracker.track(deduped);
}
看一下调用链路。
WorkManager.enqueue -> WorkManagerImpl.enqueue -> WorkContinuationImpl.enqueue -> EnqueueRunnable.run -> run()#addToDatabase()
跟进addToDatabase,看到这里就应该差不多明白了,数据库开启了事务,在这里增删改查操作是具有原子性的。插入没有完成,查询只能等待咯。
public boolean addToDatabase() {
WorkManagerImpl workManagerImpl = mWorkContinuation.getWorkManagerImpl();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
workDatabase.beginTransaction();
try {
boolean needsScheduling = processContinuation(mWorkContinuation);
workDatabase.setTransactionSuccessful();
return needsScheduling;
} finally {
workDatabase.endTransaction();
}
}
2.5 LiveData的去重处理。
public static <In, Out> LiveData<Out> dedupedMappedLiveDataFor(
@NonNull LiveData<In> inputLiveData,
@NonNull final Function<In, Out> mappingMethod) {
final Object lock = new Object();
final MediatorLiveData<Out> outputLiveData = new MediatorLiveData<>();
outputLiveData.addSource(inputLiveData, new Observer<In>() {
Out mCurrentOutput = null;
@Override
public void onChanged(@Nullable final In input) {
ThreadPool.getInstance().addTask(() -> {
synchronized (lock) {
Out newOutput = mappingMethod.apply(input);
if (mCurrentOutput == null && newOutput != null) {
mCurrentOutput = newOutput;
outputLiveData.postValue(newOutput);
} else if (mCurrentOutput != null
&& !mCurrentOutput.equals(newOutput)) {
mCurrentOutput = newOutput;
outputLiveData.postValue(newOutput);
}
}
});
}
});
return outputLiveData;
}
还未完,待更新。。。
推荐阅读更多精彩内容
- Java进阶篇:多线程并发实践 Java进阶篇:多线程并发实践 关于作者 郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢... 郭孝星
- 《Java并发编程的艺术》笔记 layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt... xiaogmail
- Java并发(2) 1.如何暂停或恢复线程 在JDK中提供了以下两个方法(类Thread)用来暂停线程和恢复线程。 Øsuspend方... pgl2011
- 【转载】线程池与五种线程池策略使用与解析 它们都是某种线程池,可以控制线程创建,释放,并通过某种策略尝试复用线程去执行任务的一个管理框架在Java8中,按照... 汤圆叔
- JAVA 线程 【JAVA 线程】 线程 进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者... Rtia