Android异步任务AsyncTask完全解析

来源:互联网 发布:网络推广途径有那些 编辑:程序博客网 时间:2024/06/07 17:47

一、概述

AsyncTask,大家应该都用过,之所以说用过,因为现在第三方开源库有不少,异步任务框架(比如RxJava等)做的都很完善,也避免了AsyncTask的一些弊病。但是AsyncTask还是有很多使用者的。而在Android开发文档中也推荐使用它来完成异步任务。

简单使用

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {    protected Long doInBackground(URL... urls) {        int count = urls.length;        long totalSize = 0;        for (int i = 0; i < count; i++) {            totalSize += Downloader.downloadFile(urls[i]);            publishProgress((int) ((i / (float) count) * 100));            // Escape early if cancel() is called            if (isCancelled()) break;        }        return totalSize;    }    protected void onProgressUpdate(Integer... progress) {        setProgressPercent(progress[0]);    }    protected void onPostExecute(Long result) {        showDialog("Downloaded " + result + " bytes");    }}

上面的例子是AsyncTask注释里的例子,我就直接套过来用了。创建任务之后,即可以开始任务:new DownloadFilesTask().execute(url1, url2, url3);

简单使用就说这么多了,本文重点在于源码的解析。

二、源码解析

1. 类注释

AsyncTask让我们可以正确使用UI线程,让我们不用操作Thread与Handler而达到执行后台操作与在UI线程公布结果的目的。作为Thread和Handler的辅助类,它不构成一个通用的线程框架。其适用于最多只耗时几秒钟的操作。

AsyncTask由三个泛型参数定义:

  • Params,传递给任务的参数类型;
  • Progress,进度值类型;
  • Result,任务返回结果类型。

如果你不想指定类型,可以使用Void来代替。

AsyncTask拥有四个主要步骤,即:

  • onPreExecute:任务执行前,在UI线程调用。比如进度条可以在这里显示;
  • doInBackground:执行后台计算,在后台线程调用。在onPreExecute执行后立即调用,Params会传递进来,执行结果会被返回给onPostExecute。除此之外,可以在执行过程中调用publishProgress来展示进度,会回调onProgressUpdate;
  • onProgressUpdate:显示进度,在UI线程调用。会在publishProgress之后调用。
  • onPostExecute:任务执行完成,处理返回结果,在UI线程调用。doInBackground的返回结果传递进来。

取消任务

通过cancel(boolean)取消任务,之后isCancelled()返回true,而且如果doInBackground返回值了,会调用onCancelled(Result),而不是onPostExecute。尽可能定期在doInBackground中用isCancelled()方法来检查是否已经取消。

线程规则

  • AsyncTask类必须在UI线程加载。4.1之后系统自动完成;
  • 任务实例必须在UI线程创建;
  • excute方法必须在UI线程调用;
  • 不要手动调用onPreExecute()、onPostExecute()、doInBackground()、onProgressUpdate()方法;
  • 每个任务只能被执行一次,否则会抛出异常。

内存可观测性

AsyncTask保证所有回调都是同步的,因此即使没有显示同步,以下操作也是安全的:

  • 在构造函数中或onPreExecute设置变量,在doInBackgroud中引用;
  • 在doInBackground中设置变量,在onProgressUpdate和onPostExecute中引用。

执行顺序

AsyncTask刚引入时,在一个后台线程中串行执行;Android1.6之后,在线程池中并行执行;Android3.0之后,则在单线程执行。想要并行执行,则调用executeOnExecutor(Executor, Params)方法执行即可。

2. 构造

public AsyncTask() {    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) {        @Override        protected void done() {            try {                postResultIfNotInvoked(get());            } catch (InterruptedException e) {                android.util.Log.w(LOG_TAG, e);            } catch (ExecutionException e) {                throw new RuntimeException("An error occurred while executing doInBackground()",                        e.getCause());            } catch (CancellationException e) {                postResultIfNotInvoked(null);            }        }    };}private Result postResult(Result result) {    @SuppressWarnings("unchecked")    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,            new AsyncTaskResult<Result>(this, result));    message.sendToTarget();    return result;}

很简单,只初始化了一个WorkerRunnable和一个FutureTask。WorkerRunnable用于在线程池执行,如代码所示,在call方法中执行了doInBackground方法,最后则执行了postResult(result)方法,其实就是将message发送到消息队列。
再看一下,getHandler()方法获取到的Handler定义:

private static class InternalHandler extends Handler {    public InternalHandler() {        super(Looper.getMainLooper());    }    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})    @Override    public void handleMessage(Message msg) {        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;        switch (msg.what) {            case MESSAGE_POST_RESULT:                // There is only one result                result.mTask.finish(result.mData[0]);                break;            case MESSAGE_POST_PROGRESS:                result.mTask.onProgressUpdate(result.mData);                break;        }    }}private static class AsyncTaskResult<Data> {    final AsyncTask mTask;    final Data[] mData;    AsyncTaskResult(AsyncTask task, Data... data) {        mTask = task;        mData = data;    }}

可以看到,首先使用Looper.getMainLooper(),指定为UI线程的Handler,之后获取到result,判断what是处理发送结果,还是更新进度。AsyncTaskResult就是将当前的AsyncTask与返回的数据结果封装起来。如果是更新进度(一般在doInBackground中手动调用publishProgress方法),则回调AsyncTask的onProgressUpdate(Progress)方法。如果是处理发送结果,回调用AsyncTask的finish方法即可。

代码如下:

private void finish(Result result) {    if (isCancelled()) {        onCancelled(result);    } else {        onPostExecute(result);    }    mStatus = Status.FINISHED;}

这里就很明了了,正如类注释所说,取消了则回调onCancelled(Result),否则回调onPostExecute(Result)方法,并且最后标记为FINISHED状态。

而FutureTask的初始化,说来很尴尬,我们追踪到postResultIfNotInvoked中,发现mTaskInvoked要为false才会直接调用postReslut,但是mTaskInvoked从一开始就被置为true,呵呵,基本就废了。

上面就展示了从doInBackground到onPostExecute,以及onProgressUpdate和onCacelled方法的调用过程,基本上就这些了。

等等,说好的onPreExecute呢?

3. Task执行

public final AsyncTask<Params, Progress, Result> execute(Params... params) {    return executeOnExecutor(sDefaultExecutor, params);}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;}

一般我们会调用task.execute(param1, param2…)来执行该操作。我们发现最终还是调用了executeOnExecutor(Executor, Params…)方法,只是指定了一个默认的默认的Executor,即SerialExecutor。顾名思义,串行Executor。这里就如注释所说,3.0之后默认的在一个线程串行执行。

正如实现所写,先判断状态,如果不是PENDING状态,说明已经或者正在执行,就会抛出异常。如果是等待,就标记为RUNNING状态,随后就执行了onPreExecute()方法。紧接着,将参数赋值给了mWorker的mParams并且调用Executor的execute方法执行任务。

三、缺陷与总结

1.缺陷

在总结之前,我们先谈谈这个AsyncTask的缺陷,相信大家也在网上有所耳闻。但是要根据版本不同来区分这个缺陷。

先看一下3.0之前的一个版本(2.2.3):
private static final int MAXIMUM_POOL_SIZE = 128;
private static final BlockingQueue<Runnable> sWorkQueue = new LinkedBlockingQueue<Runnable>(10);

在模拟器上跑,抛出异常:java.util.concurrent.RejectedExecutionException: pool=128/128, queue=10/10。
这么一看,阻塞队列长度为10,线程池最大并发数为128,这个就是缺陷所在了,任务过多,超过128+阻塞对列就会出问题。

再看3.0之后的:
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable> (128);
这个我测试了,结果并没有出现问题,即使超过了128很多,仍然可以继续跑下去,直到执行完全部任务。而在源码中可以发现,其使用了串行的SerialExecutor,其中当任务添加时是放到了ArrayDeque中,因此并不会超过上限。

本身这里我提出一个问题,就是3.0之后使用executeOnExecutor(Executor , Params…)方法时,AsyncTask默认实现的THREAD_POOL_EXECUTOR比以前版本其实只是交换了一下线程池最大数与队列数,在真机上(华为荣耀7,Android6.0)跑居然不会跑死,何解?我原以为是Executor实现不一样了,后来一看,区别有但是不大,逻辑是差不多的,瞬间蛋疼。之后在网上荡、挖源码一天也没发现这个,后来晚上回去,发现了一个分析AysncTask两种线程池的博文,使用并发执行时,确实抛出了异常,我才想会不会是我大厂的机器给它动了手脚,一测试,果不其然,模拟器,测试机,各种Android版本全部试了一遍,发现只有我这部(荣耀7)有问题(队列满就阻塞,一直到全部添加完),尴尬至极。

大概分析一下ThreadPoolExecutor的execute方法源码:

/** * Executes the given task sometime in the future.  The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {@code RejectedExecutionHandler}. * * @param command the task to execute * @throws RejectedExecutionException at discretion of *         {@code RejectedExecutionHandler}, if the task *         cannot be accepted for execution * @throws NullPointerException if {@code command} is null */public void execute(Runnable command) {    if (command == null)        throw new NullPointerException();    /*     * Proceed in 3 steps:     *     * 1. If fewer than corePoolSize threads are running, try to     * start a new thread with the given command as its first     * task.  The call to addWorker atomically checks runState and     * workerCount, and so prevents false alarms that would add     * threads when it shouldn't, by returning false.     *     * 2. If a task can be successfully queued, then we still need     * to double-check whether we should have added a thread     * (because existing ones died since last checking) or that     * the pool shut down since entry into this method. So we     * recheck state and if necessary roll back the enqueuing if     * stopped, or start a new thread if there are none.     *     * 3. If we cannot queue task, then we try to add a new     * thread.  If it fails, we know we are shut down or saturated     * and so reject the task.     */    int c = ctl.get();    if (workerCountOf(c) < corePoolSize) {        if (addWorker(command, true))            return;        c = ctl.get();    }    if (isRunning(c) && workQueue.offer(command)) {        int recheck = ctl.get();        if (! isRunning(recheck) && remove(command))            reject(command);        else if (workerCountOf(recheck) == 0)            addWorker(null, false);    }    else if (!addWorker(command, false))        reject(command);}

其实也不用多说了,注释上都写的很清楚,分三步处理:

  • 如果运行的线程数少于corePoolSize,启动新线程并将Runnable作为第一个task,并对runState和workerCount进行原子检查,以防错误警报和不该添加时添加线程。
  • 如果task入队列了,还有双重检查是否要添加线程(前面一次检查后线程可能死亡了)或者线程池关闭。所以重新检查状态,如果停止,则回滚入队,或者如果没有,则启动新线程。
  • 如果task不能入队列,会尝试新建线程,如果失败,则认为是关闭或者饱和了,抛出reject异常。

再来分析预测一下荣耀7的实现。我们看到上面的代码中,入队列使用的是workQueue.offer(command),这个方法是立即返回的,入队失败就是false,入队成功就是true,而真机上却是一直等待到队列不满时才会继续添加,就是阻塞了。因此我猜想,大厂应该是使用了workQueue.put(command),对于BlockingQueue,使用offer添加任务是直接将task添加到对尾,成功则返回true,如果队列满了,就返回false,Executor会往下执行抛出异常。而对于put添加任务,如果队列满了,就会挂起线程等待,直到队列可以添加了才会唤醒线程继续添加,这样就像我的机器上的效果一样,不会抛异常,但是要等很久。(我测试的时候创建了1000个task,然后每个sleep10s,而且是在onCreate中干的,所以一直就是白屏,不会显示内容)

2. 总结

从本文大家可以看到,如何使用AsyncTask,以及AsyncTask的基于线程池的实现原理,当然还有整个回调的执行过程,最后也涉及到了AsyncTask的缺陷问题。

原创粉丝点击