AsyncTask,Android 开发者应该都知道,可以方便的实现后台任务并回调结果给 UI Thread。对这个类稍微熟悉点的应该还知道 AsyncTask 只适用于短时后台任务,容易造成内存泄漏甚至 Crash。但这是为什么呢,为什么只适用于短时后台任务,又为什么容易造成内存泄漏?简单来说就是Google 给它的功能定位就是这样的,所以在实现上就有了一定的限制。我们看文档
AsyncTask is designed to be a helper class around
Thread
andHandler
and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by thejava.util.concurrent
package such asExecutor
,ThreadPoolExecutor
andFutureTask
.
文档中说的很清楚,AsyncTask 是封装了 Thread 和 Handler 的一个辅助类,不适合用来实现通用的线程模型,适合用于最多耗时几秒钟的后台操作。如果需要实现长时间的后台任务,建议使用 JDK 提供的并发 API。
关于执行策略,文档中是这么说的:
When first introduced, AsyncTasks were executed serially on a single background thread. Starting with
DONUT
, this was changed to a pool of threads allowing multiple tasks to operate in parallel. Starting withHONEYCOMB
, tasks are executed on a single thread to avoid common application errors caused by parallel execution.If you truly want parallel execution, you can invoke
executeOnExecutor(java.util.concurrent.Executor, Object[])
withTHREAD_POOL_EXECUTOR
.
Android-1.5 (API level 3):串行执行,即只能执行完一个任务再继续下一个任务;
Android-3.0 (API level 11) 之前:线程池并发执行;
-
Android-3.0 开始,默认串行,但可以通过
executeOnExecutor(java.util.concurrent.Executor, Object[])
配合THREAD_POOL_EXECUTOR
参数实现并行。
但这里文档和实际代码有点出入,从代码中看应该是 "After HONEYCOMB" 而不是 "Starting with HONEYCOMB"。
我们看 HONEYCOMB (android-3.2.4, API lever 13) 的代码:
/**
* Executes the task with the specified parameters. The task returns
* itself (this) so that the caller can keep a reference to it.
*
* <p>Note: this function schedules the task on a queue for a single background
* thread or pool of threads depending on the platform version. When first
* introduced, AsyncTasks were executed serially on a single background thread.
* Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. After
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this
* back to a single thread to avoid common application errors caused
* by parallel execution. If you truly want parallel execution, you can use
* the {@link #executeOnExecutor} version of this method
* with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings on
* its use.
*
* <p>This method must be invoked on the UI thread.
*
* @param params The parameters of the task.
*
* @return This instance of AsyncTask.
*
* @throws IllegalStateException If {@link #getStatus()} returns either
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
*/
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(THREAD_POOL_EXECUTOR, params);
}
代码里默认其实还是 Thread poll 并发执行的,而且注释里面也说了:
After {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this back to a single thread to avoid common application errors caused by parallel execution.
计划在 HONEYCOMB 之后改为单线程。再看看 Ice Cream Sandwich (android-4.0.1, API level 14) 源码:
/**
* Executes the task with the specified parameters. The task returns
* itself (this) so that the caller can keep a reference to it.
*
* <p>Note: this function schedules the task on a queue for a single background
* thread or pool of threads depending on the platform version. When first
* introduced, AsyncTasks were executed serially on a single background thread.
* Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. After
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this
* back to a single thread to avoid common application errors caused
* by parallel execution. If you truly want parallel execution, you can use
* the {@link #executeOnExecutor} version of this method
* with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings on
* its use.
*
* <p>This method must be invoked on the UI thread.
*
* @param params The parameters of the task.
*
* @return This instance of AsyncTask.
*
* @throws IllegalStateException If {@link #getStatus()} returns either
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
*/
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
这里引入了一个新的静态变量 sDefaultExecutor :
/**
* An {@link Executor} that executes tasks one at a time in serial
* order. This serialization is global to a particular process.
*/
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
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);
}
}
}
所以这里虽然最终还是用 Thread pool 执行,但通过 SerialExecutor 转换成串行执行。
通过上面这些还可以大概解释为什么AsyncTask 只适合做短时后台任务,因为是串行,所有如果有耗时很长的 AsyncTask 任务,那他后面的其它任务就会被阻塞。那我们可能又会想到不用默认的 Executor,而改用 THREAD_POOL_EXECUTOR
呢?其实去看源码里面,AsyncTask 里面的这个 THREAD_POOL_EXECUTOR
它的 CORE_POOL_SIZE
,即会长时间保留的空闲线程数量是比较少的,这样的话,如果有长时间后台任务,那势必长时间占用线程池中的线程,后续的任务如果较多的话可能就要频繁的创建和销毁线程,我们知道创建和销毁线程是开销比较大的,所以这里会有一定的性能问题。
还有一个问题,容易造成内存泄漏甚至 Crash,这个其实是使用不当造成的。我们看 AsyncTask 中一些关键的方法:
-
onPreExecute()
: 运行在 UI 线程,任务开始执行前的回调,可以用来在界面上显示一个 progress bar 之类的操作 -
doInBackground(Params...)
: 运行在后台线程,也就是我们需要在后台运行的任务代码 -
onProgressUpdate(Progress...)
: 运行在 UI 线程,在进度有更新的时候回调 -
onPostExecute(Result)
: 运行在 UI 线程,后台任务完成之后的结果回调
上面这几个方法也说明的 AsyncTask 的一个主要功能特点,就是简化了后台任务和 UI 线程的通信操作,不用我们手动写 Thread、Handler 相关代码。因为 AsyncTask 通常都会需要操作一些 UI 元素,比如显示及更新 progress bar,或者把结果更新到界面等,为了简化代码,我们很可能就直接在 Activity 里面创建一个继承 AsyncTask 的内部类,这样就能直接使用 Activity 里面的 UI 元素,但这样有一个问题就是因为在内部类里面会持有 Activity 实例的引用,这样就导致假如在 AsyncTask 执行期间 Activity 被销毁(比如用户按了返回键),但因为 AsyncTask 还在运行,它又持有 Activity 引用,所以这个 Activity 不能及时被 GC 回收。更严重的是因为这个时候 View 已经从 Window 上面移除了,所以后续在 onProgressUpdate(Progress...)
或者 onPostExecute(Result)
中对 View 的操作可能触发异常,从而导致 App crash,这个问题即使不用内部类的形式也同样会有。但如果代码逻辑比较严谨,有相应的防范措施,AsyncTask 还是可以放心使用的。