SAKA'S BLOG

AsyncTask深入讲解

上一节讲了asynctask的官方文档,这一节深入讲解如何深入使用AsyncTask。

asynctask本质上也是线程启动,只是它封装了一些内容,可以运行在后台,同时可以和UI线程交互。
asynctask最少要启动2个线程,最多四个。

AsyncTask的状态

AsyncTask的状态共有三种,PENDING,RUNNING和FINISHED。这三种状态在AsyncTask的生命周期中之出现一次。

  1. PENDING,表示异步任务还没有开始运行。
  2. RUNNING,表示异步任务正在运行。
  3. FINISHED,表示onPostExecute已经执行完成。

安卓官方文档推荐我们编写一个子类继承自AsyncTask并且必须实现doinbackground方法。
我们编写一个简单的程序测试这三个状态的具体情况。

1
2
3
4
5
6
7
8
private ProgressTask task = new ProgressTask();//自定义的一个内部类,继承自AsyncTask
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
...
Log.d("task", "before execute task's status is " + task.getStatus());
task.execute();
Log.d("task", "after execute task's status is " + task.getStatus());

在AsyncTask中我们实现以下几个方法

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onPreExecute() {
super.onPreExecute();
Log.d("task", "onPreExecute task's status is " + task.getStatus());
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
Log.d("task", "onPostExecute task's status is " + task.getStatus());
handler.sendEmptyMessage(1);
}

在handler中我们接收一个信息,用来打印此时task的状态

1
2
3
4
5
switch (msg.what) {
case 1:
Log.d("task", "finally task's status " + task.getStatus());
break;
}

这样,我们的程序运行起来看一下日志

1
2
3
4
5
04-02 18:10:43.747 10433-10433/com.example.saka.materialdesignapplication D/task: before execute task's status is PENDING
04-02 18:10:43.748 10433-10433/com.example.saka.materialdesignapplication D/task: onPreExecute task's status is RUNNING
04-02 18:10:43.748 10433-10433/com.example.saka.materialdesignapplication D/task: after execute task's status is RUNNING
04-02 18:11:17.724 10433-10433/com.example.saka.materialdesignapplication D/task: onPostExecute task's status is RUNNING
04-02 18:11:17.724 10433-10433/com.example.saka.materialdesignapplication D/task: finally task's status FINISHED

可以看到,在整个任务周期中,task在execute之前是一直处于pennding状态,
在execute之后onPostExecute中的所有方法执行完成之前一直处于RUNNING状态,
跳出onPostExecute方法之后所有的task任务相当于已经完成,
这时候task的状态时FINISHED。
通过观察源码我们可以看一下:
当你新建一个AsyncTask对象以后,系统就会自动生成一个变量:
private volatile Status mStatus = Status.PENDING;
这也就是说明当你new一个AsyncTask后,它的状态就被设置为了PENDING状态。
直到execute或者executeOnExecutor方法执行之后。
调用task.execute()之后,系统会调用executeOnExecutor()方法:

1
2
3
4
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}

传入的参数表明会启动默认的线程执行器,而现在系统默认的串行执行,也就是分先后次序执行。
executeOnExecutor()方法返回的同样是一个AsyncTask实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}

mStatus = Status.RUNNING;

onPreExecute();

mWorker.mParams = params;
exec.execute(mFuture);

return this;
}

在这个方法中,首先判断异步任务的状态,
假如这个任务没有在等待状态,而是正在运行或者已经结束,会抛出异常。
然后会将异步任务的状态设置为RUNNING,调用onPreExecute方法,
并将参数传递给AsyncTask中的参数接收器,
然后才开始执行exec.execute(mFuture)方法。所以在onPreExecute之前,状态已经设置为了RUNNING。
再来研究以下何时会停止。
调用exec.execute(mFuture)中传入的参数是一个接口类型的Runable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
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;
}
};

mFuture = new FutureTask<Result>(mWorker)
上面两步都是在new一个AsyncTask实例的时候执行的,try块中时执行doInbackGround方法,
在finally中执行的是结束动作。在postResult()方法中,定义了一个message,
调用该方法的时候会使用AsyncTask中的handler发送消息,handler接收到消息后执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
...
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}

在finish方法中设置了status为FINISHED,
注意此处时在执行完onCancelled或者onPostExecute之后才改变状态,
这也就解释了为什么在onPostExecute方法中获取的状态仍然是RUNNING。

Note:根据文档中的介绍,并没有讲当task被cancel后是否会执行FINISHED,实际是执行的。

取消任务

关于cancel方法官方的介绍比较简单,参看上篇文章。
首先看一下官方文档对这个API的解释:
boolean cancel (boolean mayInterruptIfRunning)

尝试取消执行此任务。 如果任务已经完成,已经被取消或由于某种其他原因而无法取消,则此尝试将失败。
如果成功,并且在取消被调用时此任务尚未开始,则此任务不应该运行。
如果任务已经启动,那么mayInterruptIfRunning参数可以确定执行该任务的线程是否应该被中断,以试图停止该任务。
调用此方法将导致在doInBackground(Object [])返回后在UI线程上调用onCancelled(Object)。
调用此方法可以保证onPostExecute(Object)从不被调用。
调用此方法后,应该从doInBackground(Object [])定期检查isCancelled()返回的值,以尽早完成任务。

这是什么意思呢?

1
2
3
4
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}

调用asynctask的cancel方法其实就是调用futuretask的cancel方法,这个是java线程中的方法,
假如传入的参数为true的时候(并不推荐这种方式),会立即停止当前任务;
当传入的参数为false的时候,会等到本次任务执行完成后,
无论在哪种情况下,我们都应该清楚的认识到,
此时获取当前任务的isCanceled都会返回为true。

调用cancel方法影响到的会有doInBackground、onPostExecute和onCancelled方法,
当传入参数为ture的时候,doInBackGround方法会立即中断,进入onCancelled方法,而不执行onPostExecute方法;
当传入的参数为false的时候,doInBackground方法会执行完毕后进入onCancelled方法,也不执行onPostExecute方法;
始终不影响onPreExecute方法,它始终会执行。
但是偶尔会出现doInbackground方法不执行而直接进入onCancelled方法。

  1. task启动直接cancel:
    1
    2
    3
    private void setCancelNow() {
    task.cancel(false);
    }

此时日志是:

1
2
3
4
04-02 21:44:24.692 8082-8082/com.example.saka.materialdesignapplication D/task: before execute task's status is PENDING
04-02 21:44:24.692 8082-8082/com.example.saka.materialdesignapplication D/task: onPreExecute task's status is RUNNING
04-02 21:44:24.692 8082-8082/com.example.saka.materialdesignapplication D/task: after execute task's status is RUNNING
04-02 21:44:24.747 8082-8082/com.example.saka.materialdesignapplication D/task: onCancelled task's status:RUNNING

可见方法执行了onPreExecute和onCancelled方法,然而并没有执行doInBackground方法。

  1. task启动后过一段时间在cancel:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    private void setCancelDelay() {
    try {
    Thread.sleep(1000);
    task.cancel(false);
    Log.d("task", "task after cancel");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

此时的输出日志是:

1
2
3
4
5
6
7
8
9
10
04-02 21:51:22.781 14525-14525/? D/task: before execute task's status is PENDING
04-02 21:51:22.781 14525-14525/? D/task: onPreExecute task's status is RUNNING
04-02 21:51:22.781 14525-14525/? D/task: after execute task's status is RUNNING
04-02 21:51:22.782 14525-14540/? D/task: doInBackground
04-02 21:51:23.113 14525-14540/? D/task: the progress value 0
04-02 21:51:23.452 14525-14540/? D/task: the progress value 1
04-02 21:51:23.782 14525-14525/com.example.saka.materialdesignapplication D/task: task after cancel
...
04-02 21:51:56.552 14525-14525/com.example.saka.materialdesignapplication D/task: onCancelled task's status:RUNNING
04-02 21:51:56.552 14525-14525/com.example.saka.materialdesignapplication D/task: finally task's status FINISHED

可以看到此时执行了doInbackground方法,最后进入了onCancelled方法。

因为preExecute方法是在异步任务启动后而真正的线程启动前调用的,
而cancel方法调用的时futuretask的cancel方法,
也就是真正启动线程之后才执行cancel的,
所以preExecute一定会执行。
doInBackground是在WorkerRunnable的call回调方法中执行的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
...
try {
...
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
}
};

假如WorkerRunnable不执行,也就不会调用onCancelled方法。
futuretask有一个回调方法是必须实现的,done()方法,当任务结束后会调用此方法。
futuretask在done的回调方法中检测mTaskInvoked是否被调用,假如未被调用会执行
postResultIfNotInvoked(get())

1
2
3
4
5
6
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}

此时同样会调用postResult进而调用onCancelled方法。

所以无论何时都会调用onCancelled方法。

任务的串行和并行

首先看看execute方法:
AsyncTask execute (Params… params)

使用指定的参数执行任务。 该任务返回自身(this),以便调用者可以保留对它的引用。
注意:此函数根据平台版本为队列中的任务排定一个后台线程或线程池。
第一次被引入时,AsyncTasks在单个后台线程上串行执行。
从DONUT开始,被更改为允许多个任务并行操作的线程池。
HONEYCOMB开始,任务改为在单线程执行,以避免并行执行引起的常见应用程序错误。
如果需要并行执行,可以使用THREAD_POOL_EXECUTOR的此方法的executeOnExecutor(Executor,Params …)方法。

这个是什么意思呢?

现在的安卓系统,基本都是大于3.0的,也就是默认情况下会按顺序执行每一个asynctask,比如,我们new出两个异步任务:

1
2
3
4
5
task = new ProgressTask();
Log.d("task", "before execute task's status is " + task.getStatus());
task.execute(10);
ProgressTask task1=new ProgressTask();
task1.execute(90);

然后打印日志输出的是task先执行完毕才执行task1,这就证明了只能运行一个异步任务,
而其他异步任务会在队列中等待第一个执行完毕才执行。

根据官方文档我们可以使用executeOnExecutor来并行执行两个任务,看一下API文档:
AsyncTask executeOnExecutor (Executor exec,Params… params)
使用指定的参数执行任务。该任务返回自身(this),以便调用者可以保留对它的引用。
此方法通常与THREAD_POOL_EXECUTOR一起使用,
以允许多个任务在由AsyncTask管理的线程池上并行运行,但是也可以使用自己的Executor进行自定义行为。
警告:允许多个任务从线程池并行运行通常不是想要的,因为它们的操作顺序没有定义。
例如,如果这些任务用于修改任何共同的状态(例如由于按钮点击而编写文件),
则不能保证修改的顺序。没有仔细的工作,在很少的情况下,较新版本的数据可能会被较旧的数据覆盖,
从而导致数据丢失和稳定性问题。这些更改最好是连续执行;
为了保证这样的工作是序列化的,无论平台版本如何,都可以使用SERIAL_EXECUTOR这个功能。
必须在UI线程上调用此方法。

简单验证一下

1
2
3
4
5
task = new ProgressTask();
Log.d("task", "before execute task's status is " + task.getStatus());
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,10);
ProgressTask task1=new ProgressTask();
task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,90);

这样执行后task和task2会并行执行,交替输出值。

1
2
3
4
5
6
7
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

THREAD_POOL_EXECUTOR维护的是一个LinkedBlockingQueue线程队列。
private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128);
这个阻塞队列的容量是128,则线程数超过128时要等旧的线程移除后才能加入队列,128是个大数目,我们就不验证了。
则此阻塞队列可以同时并发128个异步任务。

而安卓系统默认的executor是SerialExecutor,就是个串行执行器。看一下实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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维护的是一个双向队列,当数组队列中有值不为空的时候,取出这个任务然后在线程池中执行。

发布进度

关于发布进度这个比较简单,早doInBackgroud方法中执行publishProgress (Progress… values)方法,
此方法会直接调用运行在UI线程中的onProgressUpdate(Progress…)方法,这时候我们就可以更新进度了。

以上就是关于asynctask的一些我的理解。