最近找实习面试中经常会被问到关于AsyncTask的一些内部机制的问题,之前也早有学习,但是还不够系统,没有形成一个体系,现在我们来完整的彻底的梳理一下吧,扫除一些知识盲点。
相信大多数Android开发者都接触过AsyncTask这个类,AsyncTask主要是用来处理一些后台的任务,说到底就是一个异步处理工具类,随着Android开发配套的开源项目越来越完善越来越好,比如网络请求时有了Volley配套OKHttp等非常好用的库,但是在一些例如和UI做交互的情况下还是需要AsyncTask来帮忙的。
那么AsyncTask到底是什么呢,阅读源码我们可以发现,AsyncTask就是一个Handler和线程池的封装,线程池用来异步处理后台任务,handler用来发送消息进行UI方面的交互。好,既然说到了源码,那么我们先来看看AsyncTask的源码。(基于API 23)
先看看AsyncTask的构造方法:
1 | private final WorkerRunnable<Params, Result> mWorker; |
首先new一个mWorker,这是一个WorkerRunnable
可以看到在WorkerRunnable中最主要是做了三步,将代表任务是否调用的原子boolean标记设为true,将这个线程的优先级设为后台线程优先级,并且丢出doInBackground处理的结果。
这个结果被丢到了postResult方法中,我们可以来看看postResult方法部分的代码。
1 | private Result postResult(Result result) { |
可以看到其实这个方法并没有对丢过来的result做什么事,丢进来的result依然原封不动的丢了出去,只是在这个过程中捎带实例化了一下AsyncTaskResult,将丢进来的这个result以及当前
这个AsyncTask一起又抛给了新初始化的AsyncTaskResult, 并且将向handler发送了一个Message。注意这里message的 what是MESSAGE_POST_RESULT, 这里是有用到的,我们后面再讲。
好,我们继续往下走,看看result丢进去的AsyncTaskResult。
1 | "RawUseOfParameterizedType"}) ({ |
可以看到AsyncTaskResult只是一个static的内部类,只是起了一个保存当前的AsyncTask对象和后台处理的result data的作用。我们再继续往下看看这里保存的result和task在哪里使用到了。之前
已经提到了AsyncTask本质上就是一个线程池和Handler的封装,现在我们都已经获得了后台处理的结果了,消息也发送了,那么我们跟着result来看看这个重要组成部分吧。
1 | private static class InternalHandler extends Handler { |
这里就非常清楚了,之前也讲过在post中发送的消息的what内容是MESSAGE_POST_RESULT, 我们看看handler中,result也是消息中传递过来的AsyncTaskResult,里面保存了当前的task和处理的结果,
进入我们预想的case,通过对应的task,处理AsyncTaskResult中的data, 这里是走到了AsyncTask的finish方法。走到这儿可能有点远了,但是不怕,我们继续往下看,其实这份代码写的还是很有条理的。
1 | private void finish(Result result) { |
注意,这里的finish方法是一个private,只有当前的task能调用,一定程度上保证了安全性。它通过对isCancelled的判断可以进入不同的结果处理流程,实际上onCancelled和onPostExecute就是我们平常使用AsyncTask经常使用的两个方法,到这一步就没什么好说了,task携带着之前一步步传过来的doInBackground的结果,来到了我们最熟悉的几个方法了。只是我们需要留意一下Status这个环节。
mStatus是一个volatile的Status对象,而Status是一个枚举类,只包括了
- PENDING, 等待状态,表明当前这个task还没有执行。
- RUNNING, 运行状态,当前task正在执行中。
- FINISHED, 结束状态,当前task已经运行结束了。
而mStatus在这个task初始时就直接设为了PENDING, 并且只有在finish方法中改变为FINISHED, 这已经是最后的环节了。那么是在哪里变为RUNNING呢,我们现在就来看看整个AsyncTask最核心的部分,线程池这一块。
看完了从task构造方法一路跟随到最后finish,我们现在再跟着我们平时的用法来一起学习线程池一部分。事实上我们在使用时,每次初始化后,会使用execute这个方法来让task跑起来。我们看看这里是发生了什么事。
1 |
|
代码很简单,主要就是携带参数,在主线程中启动起来。注意,这里只能是在UI线程也就是主线程中完成这一步,这也是AsyncTask一个比较大的软肋。
事实上task是通过一个队列完成的。别急,好饭不怕晚,我们先看看这里return 的executeOnExecutor方法干了什么。
1 |
|
这里是不是就和之前一直到finish的分析连接上了呢。这里执行task时,讲道理的话到这一步时当前的task一定还是PENDING状态,所以这里会先检查mStatus,如果不是PENDING就会抛出异常,否则就正常执行,把mStatus改为RUNNING状态。然后就到了我们也非常熟悉的方法了,onPreExecute,这里一般做一些后台任务之前的事情。
终于到了重点了,完成了准备工作后,注意exec.execute(mFuture)这一步,这个exec是什么? 一个Executor,Executor只是一个接口,定义了一个execute的方法,这个exec是在上一步execute时传入的全局sDefaultExecutor。sDefaultExecutor是一个默认线程池。
1 | public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); |
可以看到,sDefaultExecutor实际上是一个SerialExecutor. 但是这里为什么要分开写呢。可以发现SERIAL_EXECUTOR是一个final对象,也就是说sDefaultExecutor默认是使用系统初始化好的SerialExecutor,但是我们也可以手动给这个Task设置一个Executor。
1 | /** @hide */ |
这样就清楚了,其实留给开发者的余地还是很大的。在这里还是只研究默认的Executor吧。一样,我们看看源码。
1 | private static class SerialExecutor implements Executor { |
哈,可以看到这里的excute方法就是我们前面分析每次提到的excute,到最后就都到了这里。首先我们可以看到在SerialExecutor中维护了一个ArrayDeque, mTasks。还记得之前在executeOnExecutor中看到的exec.execute(mFuture)方法吗?mFuture是一个FutureTask,实现了Future和Runnable,这里就将丢过来的mFutuer封装成了一个Runnable对象,然后在把这个Runnable添加到队列。
接着包含一个scheduleNext() 方法。scheduleNext是先从mTasks队列中取出队首的一个Runnable任务叫做mActive,如果这个Runnable不为空,就将其添加到真正的线程池THREAD_POOL_EXECUTOR之中执行。
需要指出的是,由于第一次execute时,在将Runnable加进队列后,mActive初始化为null,所以会默认走进scheduleNext,这样也保证了一开始的自动启动。
以后的任务就是在try{}finally{}中可以看到,每个Runnable运行完后就进入finally执行队列中的下一个任务。
我们已经发现了这里的每个Runnable都是在THREAD_POOL_EXECUTOR中完成的。这是什么?一个ThreadPoolExecutor. 我们先看看一个ThreadPoolExecutor的构造方法:
1 | public ThreadPoolExecutor(int corePoolSize, |
我们看看依次需要的参数:
- corePoolSize 核心线程池大小,也就是线程池中可以维持的线程数目,即使这些线城是空闲的,也不会终止(除非设置了allowCoreThreadTimeOut),因此可以理解为常驻线程池的线程数目。
- maximumPoolSize 线程池中允许的最大线程数目。因为一般来说线程数目越多,调度所用的花销越大,所以需要设置一个数目上限。
- keepAliveTime 当线程数目大于核心线程数目时,如果超过这个keepAliveTime时间,那么空闲的线程会被终止。
- unit keepAliveTime的时间单位。
- workQueue 一个保存尚未执行的线程的队列。这个队列只保存由execute方法提交的Runnable任务。
- threadFactory 用来构造线程池的工厂。
我们再看看ThreadPoolExecutor的execute方法。
1 | public void execute(Runnable command) { |
我们可以看一下这个线程池调度的大致流程:
- 如果线程池中的线程数目小于corePoolSize,那么就新启动一个线程,并且将这个Runnable作为这个新线程的第一个任务添加到线程中进行执行
- 如果线程池中的线程数目大于等于corePoolSize,就将任务添加到workQueue队列中等待。这种情况下,会两次确认线程池的状态,如果第2次读到的线程池状态和第1次读到的线程池状态不同,就从队列中删除该任务。
- 如果这个队列已经满了,就在线程池中新建一个线程,并将该任务添加到线程中进行执行。如果执行失败,就通过reject()拒绝该任务。
总之可以发现,线程池ThreadPoolExecutor通过workQueue来管理线程和任务,每个线程在启动后,会执行线程池中的任务;当一个任务执行结束后,它会从线程池workQueue中取出任务来继续执行。workQueue是管理线程池任务的队列,当添加到线程池中的任务超过线程池的最大线程数目时,这个任务就会进入阻塞队列进行等待。
好,我们现在来看看android中的THREAD_POOL_EXECUTOR是怎么走的。
1 | public static final Executor THREAD_POOL_EXECUTOR |
我们看看传进去的参数:
1 | //cpu的数量,Runtime获取 |
可以看到,在构建THREAD_POOL_EXECUTOR时是基于android平台的特点来的。核心线程数是设为了CPU_COUNT+1。
但是实际上在之前的android API版本中,这些值是直接设为了固定的值。
1 | /*4.3 版本的AsyncTask源码 */ |
从4.4版本改成了目前的配置。另外可以看到,4.4之前的sPoolWorkQueue的大小也只有10。这些配置的更改给AsyncTask的动态调度还是带来了优势的。比如在4.3中,核心线程数目只有5,因此如果开了5个线程后,再继续开的话就只能等待了。
这是对线程池的初始化和调度分析了,之后就是线程的执行,并且接着我们最初的流程,最后走到task的finish方法,需要我们在AsyncTask实现时完成onPostExecute就可以了。关于Future部分的分析,我们以后再讲。
实际上,面试时面试官会经常问一个问题就是AsyncTask是可以并行的吗?
经过我们上面的分析,已经可以确定AsyncTask内部有一个线程池来进行线程的调度管理以及执行。那么AsyncTask是可以并行执行的吗?先卖个关子,看看2.3.7的AsyncTask部分的代码:
1 | public final AsyncTask<Params, Progress, Result> More ...execute(Params... params) { |
看到没,在2.3.7上execute(Params… params)这个方法和6.0上有什么区别?
我们在之前已经分析过了,execute(params)是走到了 return executeOnExecutor(sDefaultExecutor, params);
而sDefaultExecutor是一个volatile的对象。
1 | private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; |
我们可以得出结论,在6.0上不是并行的,在2.3上是并行的。实际上阅读多个版本的源码可以发现,这一变化是在3.0上做到的。因此我们可以理解为从3.0之后就变成串行了,1.5-2.3是并行的。最初也是串行的。
事实上,也可以自己写demo测试一下,看看每个task的执行的时间是不是同时开始。
那么面试官又会问了,为什么在目前的版本上要将AsyncTask设计成串行呢?
揣摩设计者的意图其实真的是一个很有意思的事情啊- -23333
我们已经提到了AsyncTask是线程池和Handler的一个封装好的工具,其实做到的功能在handler message和loop也能做到,那么为什么要设计一个这样的工具呢,主要还是方便开发者的使用,尤其是初级开发者。
而可能随着android版本的迭代开发,发现有开发者很少在doInBackground中做线程安全的考虑,既然很少有人会考虑最资源的并发访问的安全性,那么干脆就不开放这个功能,保证每个线程的串行执行。这样就是皆大欢喜。
好了,对AsyncTask的分析就到这儿了,大家应该清楚整个的流程以及其特点了。但是可能大家也看到一些大牛说了,并不推荐使用默认的AsyncTask,因为实在有一些不能忽视的缺点啦- -
首先,默认的AsyncTask的线程池中的核心线程数是有限的,不管是和CPU数目有关还是以前的固定的5, 还是有一个数量的限制,因此不适合大量的后台任务处理,例如瀑布流图片的加载等。
其次,AsyncTask类必须在主线程初始化,必须在主线程创建,因为return executeOnExecutor(sDefaultExecutor, params)这里也只能在UI线程走。
最后,我们也讲到了,AsyncTask在3.0后改成了串行的,因此想真正做一些并行的后台任务,就不太适合了。
总之,大家想要更好的使用AsyncTask,最好自己修改一下再使用啦。目前默认的AsyncTask确实还不是最佳状态。