轻松管理你的后台任务—WorkManager(进阶篇)

Jetpack 是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。Jetpack 包含与平台 API 解除捆绑的 androidx.* 软件包库。这意味着,它可以提供向后兼容性,且比 Android 平台的更新频率更高,以此确保您始终可以获取最新且最好的 Jetpack 组件版本。
结合自己的阅读源码、实战经历总结。记录分享一下。

预备知识

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);
    }
  1. 创建好数据库,WorkManager正式版数据库操作都在子线程中操作,如果你想在主线程操作,那么需要allowMainThreadQueries()。
   if (useTestDatabase) {
            builder = Room.inMemoryDatabaseBuilder(context, WorkDatabase.class)
                    .allowMainThreadQueries();
        } else {
            builder = Room.databaseBuilder(context, WorkDatabase.class, DB_NAME);
        }
  1. 创建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");
        }
  1. 创建Processor,控制中心,Scheduler最后都会殊途同归,调用Processor.startWork(),去执行业务方写的Worker中的逻辑。
  2. 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进阶篇:多线程并发实践 关于作者 郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢...
    郭孝星阅读 3,021评论 0 20
  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 2,272评论 1 13
  • 1.如何暂停或恢复线程 在JDK中提供了以下两个方法(类Thread)用来暂停线程和恢复线程。 Øsuspend方...
    pgl2011阅读 461评论 0 0
  • 它们都是某种线程池,可以控制线程创建,释放,并通过某种策略尝试复用线程去执行任务的一个管理框架在Java8中,按照...
    汤圆叔阅读 2,330评论 1 43
  • 【JAVA 线程】 线程 进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者...
    Rtia阅读 933评论 2 19