AsyncTask 与 RxJava
在这个 RxJava 已经遍布各个 app 的时代,作为原生的 AsyncTask 可能已经倍感压力了吧。但是无论如何 RxJava 如何流行,AsyncTask 都是我们需要掌握的,相比于 RxJava 我们需要知道 AsyncTask 的优点是什么,这样才能让我们在解决需求的时候有一个更好的选择。AsyncTask 相比于 RxJava:
- 更轻量:AsyncTask 相比于 RxJava 适用范围更小,更专一,所以 AsyncTask 在某种程度上来说比起 RxJava 来说 API 更加友好
- 原生:类同于第一点,这样的话就不需要额外导入包,相应的应用体积也会减小
Hello World
示例类如下:
public class MainActivity extends AppCompatActivity {
private final String DOWNLOAD_URL = "https://ss1.bdstatic" +
".com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2285378925,1851556142&fm=117&gp=0.jpg";
private Button mDownloadButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDownloadButton = (Button) findViewById(R.id.btn_download);
mDownloadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
InnerAsyncTask task = new InnerAsyncTask();
task.execute(DOWNLOAD_URL);
}
});
}
private static class InnerAsyncTask extends AsyncTask<String, Integer, Boolean> {
public InnerAsyncTask() {
super();
}
@Override
protected void onPreExecute() {
Log.e("TAG", "onPreExecute: 执行前开始,线程是 " + Thread.currentThread().getName());
}
@Override
protected Boolean doInBackground(String... params) {
Log.e("TAG", "doInBackground: 执行中,线程是 " + Thread.currentThread().getName());
try {
URL url = new URL(params[0]);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int contentLength = connection.getContentLength();
InputStream inputStream = connection.getInputStream();
File file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES
+ "/test.jpg");
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int read = -1;
int length = 0;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
publishProgress((length += read) * 100 / contentLength);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
Log.e("TAG", "onProgressUpdate: " + values[0] + ",线程是 " + Thread.currentThread().getName());
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
Log.e("TAG", "onPostExecute: download success,线程是 " + Thread.currentThread().getName());
} else {
Log.e("TAG", "onPostExecute: download failed,线程是 " + Thread.currentThread().getName());
}
}
}
}
当然,这是一个 Hello World 级别的代码。通过点击 Activity 中的按钮后,我们的控制台将会输出如下:
所以可以看到的是调用顺序为:onPreExecute()
-> doInBackground()
-> onProgressUpdate()
-> onProgressUpdate()
-> onProgressUpdate()
-> …. -> onPostExecute()
同时我们也可以清楚地看到,除了耗时的 doInBackground()
方法不是在主线程之外,其他方法都是在主线程调用的,可见 AsyncTask
在小型的异步通信上真是好使!既然如此,我们就剥开 AsyncTask 的源码一探究竟吧 ——
源码简析
构造函数
mWorker
我们看源码得有个入口,既然了解了上面的 Hello World 例子,那么我们就根据该例子进行着手 ——
InnerAsyncTask task = new InnerAsyncTask();
task.execute(DOWNLOAD_URL);
简直是简单的有些过分,构造函数 + execute()
方法,首先我们戳开构造函数一览 ——
实际上也就是初始化了两个变量,我们来看第一个变量 —— mWorker
mWorker
是一个继承了 WorkerRunnable<Params, Result>
抽象类的具体实现类,而 WorkerRunnable<Params, Result>
又实现了 Callable<Result>
接口,所以 mWorker
的数据结构如下:
public class Worker extends WorkerRunnable<Params, Result> {
Params[] mParams;
@Override
public Result call() throws Exception {
}
}
请注意:泛型 Params
是 AsyncTask
的第一个泛型参数,对应上面例子的 String
,而 Result
泛型是 AsyncTask
的第三个泛型参数,对应上面例子的 Boolean
。我们来看看具体的 call()
方法的业务逻辑——
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
1.首先第一行是将 mTaskInvoked
设为 true,mTaskInvoked
是什么?是一个 AtomicBoolean
的实例化对象,通过全局搜索我们也会发现,mTaskInvoked
的值只有在这里被更改了,这意味着对于同一个 AsyncTask
对象来说,在初始化 mTaskInvoked
的时候这个值为 false,而在经过了 mWorker
的 call()
函数调用后就变为了 true,并且从此之后不会再被修改,这里是为下文埋下伏笔(#1)。
2.接下来第二行,初始化一个 Result 对象。接下来我们直接看到第六行,这个 result 将会被 doInBackground(mParams)
方法赋值,没错!就是我们覆写的那个 doInBackground()
方法,传入的参数是什么?参考 mWorker
的数据结构我们就会知道,这个参数也就是通过初始化 AsyncTask 传入的第一个泛型参数。
3.接下来我们继续向下看,如果上面的过程没有抛出异常的话,那么 mCancelled
变量(同为 AtomicBoolean
对象)仍为 false,如果抛出了异常,则为 true(#2)。做完这一步我们再看到 12 行代码的 postResult(result)
方法 ——
原来仅仅是调用了 Handler 发送了一个 MESSAGE_POST_RESULT
的消息啊,那么这个消息发送给了 Handler 之后是怎么处理的?看看 Handler 的处理策略 ——
看到这里有一丝丝小注意的地方是 —— 我们可以从 InternalHandler
的构造函数发现其实这获取的是主线程的 Handler,对于 MESSAGE_POST_RESULT
的处理方式是调用 mTask
的 finish()
方法,mTask
是什么?mTask
实际上就是 AsyncTask
对象,想要具体了解的戳开看一看就知道了,这里就不扩展了,无关源码贴多了容易乱了思路。所以我们接下来走向 finish()
方法——
根据#2我们知道,如果在未抛出异常的情况下,result
的值是有效的,那么就调用 onPostExecute()
方法,当然,不要忘了这个方法是在主线程调用的。看到这里小结一下,由于我们在新建一个 AsyncTask 对象时,在 doInBackground()
方法中手动调用 publishProgress()
方法来调用我们重写的 onProgressUpdate()
方法。那么为什么不是直接调用 onProgressUpdate()
而是要拐弯抹角通过调用 publishProgress()
来调用 onProgressUpdate()
呢?答案很简单 —— 线程不同。我们戳开 publishProgress()
方法看看 ——
doInBackground()
方法和 publishProgress()
方法都不是在主线程调用,所以需要通过主线程的 Handler 来调用 onProgressUpdate()
方法来达到目的。
所以在 mWorker
的 call()
方法中我们能确定 doInBackground()
-> publishProgress()
-> onPostExecute()
(#3)的调用顺序了。
到这里我们总算是吧 mWorker
这个对象给解释清楚了,那么我们再向下走,看看 mFuture
对象都干了些什么 ——
mFuture
其实源码很简单,mFuture
也不是一个什么复杂的对象,它仅仅是将之前的 mWorker
对象封装了一下,一步是将 mWorker
对象赋给自己的 callback
对象,一步是将 state
属性设为 NEW
(#6)。既然 mFuture
对象覆写了父类的 done()
方法,我们就来看看这个方法都做了些什么,我们可以发现理想情况下仅会调用 postResultIfNotInvoked()
方法 ——
结合 #1 我们可以知道,如果在调用 mFuture
对象的 done()
方法前调用了 mWorker
对象的 call()
方法的话,那么就不会有任何操作(#4)。
最后我们还可以稍稍注意一下注释:This constructor must be invoked on the UI thread.
笔者就不翻译了。
execute
前面的流程都是基于假设 mWorker
、mFuture
对象的该方法都被调用的情况下,但是初始化构造函数肯定是不会执行这些对象的方法的,那么什么时候该些对象的这些方法会被执行呢?回到我们的 Hello World 例子,创建完 AsyncTask
对象后,我们调用了该对象的 execute()
方法 ——
execute()
方法实际上是执行 executeOnExecutor()
方法,那么我们就看看 executeOnExecutor()
方法源码 ——
源码还是非常简单的,首先是判断当前 AsyncTask
的状态,这个判断源码还是很简单的,笔者就不在这里进行讲解了。接下来就是将当前 AsyncTask
设置成 RUNNING
状态。再就是执行 onPreExecute()
方法了!所以我们可以发现,正常使用一个 AsyncTask
对象最先执行的方法是 onPreExecute()
方法(#5),根据该方法的注释 This method must be invoked on the UI thread.
我们知道 onPreExecute()
方法是在主线程执行的。然后是将传入的 params
参数赋值给 mWorker
对象,那么传入的 params
对象是什么?参考我们上方的例子的话可以知道,这个 params
参数实际上就是下载链接的 URL。再下一步是关键的一步,调用 exec
对象的 execute(Runnable)
方法,并传入 mFuture
参数。exec
是什么?是传入的第一个形参,具体点我们再跟踪一下就会发现:
原来就是一个 Executor
接口的一个具体实现类的对象(根据这个具体实现类的 SerialExecutor
类名就可以知道这是一个串行执行线程池),那么执行它的 execute()
方法又会发生什么呢,由上面的源码可以看出——
1.第一步是向 mTasks
对象中添加一个新的 Runnable
对象,这个 Runnable
对象实际上的 run()
方法实际上是对传入的 Runnable
对象(mFuture
)的一个封装,除了执行 Runnable
对象(mFuture
)的 run()
方法之外,还要执行 scheduleNext()
方法,而看到 scheduleNext()
方法实际上就是出栈,然后调用 THREAD_POOL_EXECUTOR
的 execute()
并将出栈的这个参数传入。原来串行的过程是这样实现的!
2.如果判断 mActivity
为 null 的话,那么就执行 scheduleNext()
方法,由上面的函数我们也可以看出,第一次进入 execute()
方法时这个参数肯定为 null
,所以就开始执行 scheduleNext()
方法进行 mTask
挨个出栈的操作并被 THREAD_POOL_EXECUTOR
执行的操作,这样整个线程池就运作起来了。
说到这里这一段有点长,笔者稍稍总结一下,首先 SerialExecutor
类中有一个任务队列叫做 mTask
,该队列中存有的都是 Runnable
对象,这里的每一个 Runnable
对象实际上都是对传入的 Runnable
对象的一个封装,除了调用传入 Runnable
对象的 run()
方法之外,还调用了自身的出栈操作,并将出栈的 Runnable
对象授予 THREAD_POOL_EXECUTOR
来处理,这样就完成了串行处理的过程。
其实说了这么多,线程池执行的还是 Runnable
的 run()
方法而已,所以让我们再回到起始点,看看传入的 Runnable
对象的 run()
方法是如何书写的 ——
我们再继续查看一下 mFuture
对象的 run()
方法 ——
根据#6我们可以知道,runnable
对象实际上就是 mWorker
对象,而 state
的值也为 NEW
1.所以接下来就是执行 mWorker
对象的 call()
方法,call() 方法在前面分析过了,在 call()
方法中我们会执行到 doInBackground()
方法等方法#3,再结合 #5 我们就可以知道 AsyncTask
方法调用流程是 onPreExecute()
-> doInBackground()
-> publishProgress()
-> onPostExecute()
。
2.当执行完 call()
方法后,会将 ran
设为 true,再接着就会调用 set(result)
方法
最终会调用到 mFuture
对象的 done()
方法,但是结合 #4 我们可以知道 done()
方法里面不会有什么实际的操作。到此源码简析就完成了。
问答
- 问:一个 AsyncTask 为什么只能执行一次?
答:其内部使用 Status 枚举变量保存任务状态,当任务状态不是
PENDING
时则抛出异常。即使不是这样,在一个 Runnable 任务执行完成后,AysncTask 也会将它置空,当第二次执行它的时候实际上拿到的是一个 null 值。问:AsyncTask 为什么必须在主线程初始化,或者说在子线程创建可能会有怎样的情况?
答:AsyncTask 在内部持有一个 Handler,在子线程创建的话,子线程默认是没有 Looper 的,所以可能会 crash
问:AsyncTask 如何实现串行执行的(3.0 以后)?
答:实际上 AsyncTask 内部是持有两个线程池来处理任务的,我们将任务添加到 AsyncTask 中,其中线程池 A 内部是持有一个 ArrayDeque 线性双向队列来存有我们的任务的,它一次性取出一个任务交给线程池 B 来进行处理,以此来达到串行执行的效果。
问:AsyncTask 能在 3.0 以后实现并发执行么?
答:能。调用
executeOnExecutor()
方法传入我们自己的线程池和任务就可以了。实际上我们常用execute()
方法也是调用executeOnExecutor()
方法来执行的,只不过它传入的线程池有些特殊,内部是持有一个 ArrayQueue 队列来一个一个的将任务交给线程池来处理,而并不是一股脑全部交给线程池来处理。问:一个 AsyncTask 对象只能执行一次任务,为什么它的线程池(上面所提到的线程池 B)大小却不是1呢(官方希望的是 2~4,最好的值是 CPU 数量 - 1,避免 CPU 满载)?
- 答:AsyncTask 内部持有的线程池 B 是一个 static 变量,所以对于任何数量的 AysncTask 对象来说,它们共用的是一套线程池 B。所以即使是对象不同,实际上共用的还是同一个线程池来处理所有的任务的,所以1个线程线程是显然不够的