AsyncTask —— Android 原生异步通信简析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ziwang_/article/details/77767867

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 {
    }
}

请注意:泛型 ParamsAsyncTask 的第一个泛型参数,对应上面例子的 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,而在经过了 mWorkercall() 函数调用后就变为了 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 的处理方式是调用 mTaskfinish() 方法,mTask 是什么?mTask 实际上就是 AsyncTask 对象,想要具体了解的戳开看一看就知道了,这里就不扩展了,无关源码贴多了容易乱了思路。所以我们接下来走向 finish() 方法——

这里写图片描述

这里写图片描述

根据#2我们知道,如果在未抛出异常的情况下,result 的值是有效的,那么就调用 onPostExecute() 方法,当然,不要忘了这个方法是在主线程调用的。看到这里小结一下,由于我们在新建一个 AsyncTask 对象时,在 doInBackground() 方法中手动调用 publishProgress() 方法来调用我们重写的 onProgressUpdate() 方法。那么为什么不是直接调用 onProgressUpdate() 而是要拐弯抹角通过调用 publishProgress() 来调用 onProgressUpdate() 呢?答案很简单 —— 线程不同。我们戳开 publishProgress() 方法看看 ——

这里写图片描述

这里写图片描述

doInBackground() 方法和 publishProgress() 方法都不是在主线程调用,所以需要通过主线程的 Handler 来调用 onProgressUpdate() 方法来达到目的。

所以在 mWorkercall() 方法中我们能确定 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

前面的流程都是基于假设 mWorkermFuture 对象的该方法都被调用的情况下,但是初始化构造函数肯定是不会执行这些对象的方法的,那么什么时候该些对象的这些方法会被执行呢?回到我们的 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_EXECUTORexecute() 并将出栈的这个参数传入。原来串行的过程是这样实现的!

2.如果判断 mActivity 为 null 的话,那么就执行 scheduleNext() 方法,由上面的函数我们也可以看出,第一次进入 execute() 方法时这个参数肯定为 null,所以就开始执行 scheduleNext() 方法进行 mTask 挨个出栈的操作并被 THREAD_POOL_EXECUTOR 执行的操作,这样整个线程池就运作起来了。

说到这里这一段有点长,笔者稍稍总结一下,首先 SerialExecutor 类中有一个任务队列叫做 mTask,该队列中存有的都是 Runnable 对象,这里的每一个 Runnable 对象实际上都是对传入的 Runnable 对象的一个封装,除了调用传入 Runnable 对象的 run() 方法之外,还调用了自身的出栈操作,并将出栈的 Runnable 对象授予 THREAD_POOL_EXECUTOR 来处理,这样就完成了串行处理的过程。

其实说了这么多,线程池执行的还是 Runnablerun() 方法而已,所以让我们再回到起始点,看看传入的 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个线程线程是显然不够的

猜你喜欢

转载自blog.csdn.net/ziwang_/article/details/77767867
今日推荐