深入理解安卓异步任务AsyncTask

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

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

AsyncTask的状态

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

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

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

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中我们实现以下几个方法

        @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的状态

        switch (msg.what) {
            case 1:
                Log.d("task", "finally task's status " + task.getStatus());
                break;
        }复制代码

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

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()方法:

    @MainThread
    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }复制代码

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

    @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:

        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接收到消息后执行如下代码:

     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()返回的值,以尽早完成任务。

这是什么意思呢?

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:

    private void setCancelNow() {
         task.cancel(false);
     }复制代码

    此时日志是:

    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方法。

  2. task启动后过一段时间在cancel:

     private void setCancelDelay() {
         try {
             Thread.sleep(1000);
             task.cancel(false);
             Log.d("task", "task after cancel");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }复制代码

    此时的输出日志是:

    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回调方法中执行的,

        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())

    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出两个异步任务:

        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线程上调用此方法。

简单验证一下

        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会并行执行,交替输出值。

    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,就是个串行执行器。看一下实现方法:

    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的一些我的理解。

    原文作者:Android
    原文地址: https://juejin.im/post/58e112e7b123db15eb680b7d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞