Android源码解读——AsyncTask
为什么需要AsyncTask?
在Android当中,当一个应用程序的组件启动的时候,并且没有其他的应用程序组件在运行时,Android系统就会为该应用程序组件开辟一个新的线程来执行。默认的情况下,在一个相同Android应用程序当中,其里面的组件都是运行在同一个线程里面的,这个线程我们称之为Main线程。当我们通过某个组件来启动另一个组件的时候,这个时候默认都是在同一个线程当中完成的。
在Android当中,通常将线程分为两种,一种叫做Main Thread,除了Main Thread之外的线程都可称为Worker Thread。
当一个应用程序运行的时候,Android操作系统就会给该应用程序启动一个线程,这个线程就是我们的Main Thread,这个线程非常的重要,它主要用来加载我们的UI界面,完成系统和我们用户之间的交互,并将交互后的结果又展示给我们用户,所以Main Thread又被称为UI Thread。
Android系统默认不会给我们的应用程序组件创建一个额外的线程,所有的这些组件默认都是在同一个线程中运行。然而,某些时候当我们的应用程序需要完成一个耗时的操作的时候,例如访问网络或者是对数据库进行查询时,此时我们的UI Thread就会被阻塞。例如,当我们点击一个Button,然后希望其从网络中获取一些数据,如果此操作在UI Thread当中完成的话,当我们点击Button的时候,UI线程就会处于阻塞的状态,此时,我们的系统不会调度任何其它的事件,更糟糕的是,当我们的整个现场如果阻塞时间超过5秒钟(官方是这样说的),这个时候就会出现 ANR (Application Not Responding)的现象,此时,应用程序会弹出一个框,让用户选择是否退出该程序。对于Android开发来说,出现ANR的现象是绝对不能被允许的。
另外,由于我们的Android UI控件是线程不安全的,所以我们不能在UI Thread之外的线程当中对我们的UI控件进行操作。因此在Android的多线程编程当中,我们有两条非常重要的原则必须要遵守:
l 绝对不能在UI Thread当中进行耗时的操作,不能阻塞我们的UI Thread
l 不能在UI Thread之外的线程当中操纵我们的UI元素
既然在Android当中有两条重要的原则要遵守,那么我们可能就有疑问了?我们既不能在主线程当中处理耗时的操作,又不能在工作线程中来访问我们的UI控件,那么我们比如从网络中要下载一张图片,又怎么能将其更新到UI控件上呢?这就关系到了我们的主线程和工作线程之间的通信问题了。在Android当中,提供了两种方式来解决线程直接的通信问题,一种是通过Handler的机制,这个时候就很可能自己会去封装一下thread+handler了,正是因为这类需求很多,google就帮我们封装了一下。其实我们也可以自己封装。
原理分析
AsyncTask是个abstract类,所以在使用时需要实现一个AsyncTask的具体实现类,一般来说会覆盖4个方法,我们以前面所说的从网络中下载一张图片,然后更新到UI控件来说明:
(1)onPreExecute():在执行后台下载操作之前调用,将下载等待动画显示出来,运行在主线程中;
(2)doInBackground():核心方法,执行后台下载操作的方法,必须实现的一个方法,运行在子线程中;这个方法是执行在子线程中的。在onPreExecute()执行完后,会立即开启这个方法。
(3)onProgressUpdate():在下载操作doInBackground()中调用publishProgress()时的回调方法,用于更新下载进度,运行在主线程中;
(4)onPostExecute():后台下载操作完成后调用,将下载等待动画进行隐藏,并更新UI,运行在主线程中;
然后在主线程中
通过上面的分析,我们可以知道,AsyncTask的构造方法和execute方法是我们分析AsyncTask的重点。
1)构造方法
AsyncTask的构造方法中
mWorker代表了AsyncTask要执行的任务,是对Callable接口的封装,意味着这个任务是有返回值的
mFuture代表了AsyncTask要执行的任务的返回结果,其实就是个FutureTask,安装FutureTask标准用法,mWorker作为Callable被传给了mFuture,那么mFuture的结果就从mWorker执行的任务中取得。仔细看mWorker,return语句返回的结果就是我们前面所说的doInBackground()的执行结果:
2)再看执行流程
查看源码execute()àexecuteOnExecutor(sDefaultExecutor, params)àexec.execute(mFuture)
到了这一步,将mFuture传递给了AsyncTask的执行器进行执行。AsyncTask的执行器缺省是sDefaultExecutor。
找到成员变量sDefaultExecutor,最终定位到
SerialExecutor是对JDK里Executor的一个实现,被声明为一个静态变量,我们仔细看SerialExecutor的实现,
内部声明了一个双端队列ArrayDeque类型的mTasks(双端队列中offer方法表示从队列尾插入,poll()表示从队列头获取元素)。
每次调用execute,就创建一个Runnable匿名内部类对象,这个对象存入mTasks,在匿名内部类的run函数里面调用传入参数r.run()。然后通过一个scheduleNext函数把mTasks里面的所有对象通过THREAD_POOL_EXECUTOR.execute(mActive)执行一遍。说穿了,也就是说SerialExecutor类会把所有的任务丢入一个容器,之后把容器里面的所有对象一个一个的排队(串行化)执行THREAD_POOL_EXECUTOR.execute(mActive);
至于这个THREAD_POOL_EXECUTOR,是这样定义的:
我们可以看到这个线程池,被声明为一个静态变量,同时初始化的参数是:
核心线程数量,这里取得是CPU个数 + 1, 第二个参数是最大线程数量,这里是CPU个数 * 2 + 1,第五个参数是缓冲区的队列,这里是个LinkedBlockingQueue,这个队列的最大容量是128。
3)结果和进度的通知
AsyncTask的执行结果和进度是怎么通知给UI线程的呢?检视mFuture
和更新进度时我们会调用的publishProgress方法
我们可以看到都调用了sHandler
说明当子线程需要和UI线程进行通信时,其实就是通过这个handler,往UI线程发送消息。
总结:
1)、线程池的创建:
在创建了AsyncTask的时候,会默认创建两个线程池SerialExecutor和ThreadPoolExecutor,SerialExecutor负责将任务串行化,ThreadPoolExecutor是真正执行任务的地方,且无论有多少个AsyncTask实例,两个线程池都会只有一份。
2)、任务的执行:
在execute中,会执行run方法,当执行完run方法后,会调用scheduleNext()不断的从双端队列中轮询,获取下一个任务并继续放到一个子线程中执行,直到异步任务执行完毕。
3)、消息的处理:
在执行完onPreExecute()方法之后,执行了doInBackground()方法,然后就不断的发送请求获取数据;在这个AsyncTask中维护了一个InternalHandler的类,这个类是继承Handler的,获取的数据是通过handler进行处理和发送的。在其handleMessage方法中,将消息传递给onProgressUpdate()进行进度的更新,也就可以将结果发送到主线程中,进行界面的更新了。
4)、使用AsyncTask的注意点
通过观察代码我们可以发现,每一个new出的AsyncTask只能执行一次execute()方法,多次运行将会报错,如需多次,需要新new一个AsyncTask。
AsyncTask优缺点
AsyncTask:
优点:AsyncTask是一个轻量级的异步任务处理类,轻量级体现在,使用方便、代码简洁上,而且整个异步任务的过程可以通过cancel()进行控制;
缺点:不适用于处理长时间的异步任务,一般这个异步任务的过程最好控制在几秒以内,如果是长时间的异步任务就需要考虑多线程的控制问题;当处理多个异步任务时,UI更新变得困难。
Handler:
优点:代码结构清晰,容易处理多个异步任务;
缺点:当有多个异步任务时,由于要配合Thread或Runnable,代码可能会稍显冗余。
总之,AsyncTask不失为一个非常好用的异步任务处理类,只要不是频繁对大量UI进行更新,可以考虑使用;而Handler在处理大量UI更新时可以考虑使用。