Android开发Android开发经验谈程序员

(二)现状篇:深度解读AsyncTask

2019-04-09  本文已影响20人  呼啸长风

一、前言

为了提高流畅性,耗时任务放后台线程运行,这是APP开发的常识了。
远古时期,还没有各种库的时候,用来处理异步任务的方法有:
Thread/ThreadPoolExecutor、Service/IntentService、AsyncTask……

其中,AsyncTask历经多次迭代,可以说是骨灰级的API了。
AsyncTask适用于“数据加载+界面刷新”的模式,而对于Android开发而言,这类模式是比较常见的,所以一度还是很多人使用AsyncTask的。
然而随着RxJava的普及,AsyncTask日渐式微,如今或许还有存在于一些旧代码中,或许还有部分开发者还在使用。
AsyncTask最终是否会被掩埋于历史的尘埃之中,不得而知;
事实上,虽是“古董”,搬出来把玩一番,拭去尘埃,你会发现,破旧的表面之下,也有熠熠生辉之处。

二、用法

先来看一段展示基本用法的代码:

public class TestActivity extends AppCompatActivity {
    private TextView mProgressTv;
    private ProgressBar mProgressBar;
    private Button mStartBtn;
    private TestTask mTestTask;
    private long mCount;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // ... 
        mStartBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTestTask = new TestTask();
                mTestTask.execute(++mCount);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mTestTask != null){
            mTestTask.cancel(true);
        }
    }

    private class TestTask extends AsyncTask<Long, Integer, String>{
        @Override
        protected void onPreExecute() {
            mProgressTv.setVisibility(View.VISIBLE);
            mProgressBar.setVisibility(View.VISIBLE);
        }

        @Override
        protected String doInBackground(Long... params) {
            for (int i = 0; i <= 100; i += 2) {
                // do something, like request data
                publishProgress(i);
            }
            return params[0] + "st done";
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            mProgressBar.setProgress(values[0]);
            mProgressTv.setText(values[0]+"%");
        }

        @Override
        protected void onPostExecute(String s) {
            mProgressTv.setText(s);
        }

        @Override
        protected void onCancelled() {
            ToastUtil.shortTips("TestTask cancel");
        }
    }
}

很简单的一段代码,所实现的是:做任务,刷新进度,显示结果。
关于用法,掌握几个参数泛型和函数即可。

从API文档我们可以知道:
前三个方法需要主动调用,其他是回调方法。
常规流程:execute -> onPreExecute -> doInBackground -> onPostExecute;
doInBackground的过程中可以调用 publishProgress 发布进度,然后onProgreessUpdate在UI线程中被回调;
如果调用了cancel,执行结束时回调onCancelled,而不回调onPostExecute。

三、原理

3.1 执行流程

AsyncTask的实现很简洁,去掉注释,只有两三百行代码;要分析流程,100行左右的核心代码就够了。
下面是精简后的代码:

public abstract class AsyncTask<Params, Progress, Result> {
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static InternalHandler sHandler;
    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    public AsyncTask() {
        sHandler = new InternalHandler(Looper.getMainLooper());

        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    result = doInBackground(mParams);
                } finally {
                    postResult(result);
                }
                return result;
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                postResultIfNotInvoked(get());
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

    private void postResult(Result result) {
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
    }

    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

    private static class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            if(msg.what == MESSAGE_POST_RESULT){
                result.mTask.finish(result.mData[0]);
            }else if(msg.what == MESSAGE_POST_PROGRESS){
                result.mTask.onProgressUpdate(result.mData);
            }
        }
    }

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
    }

    public final AsyncTask execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    public final AsyncTask executeOnExecutor(Executor exec, Params... params) {
        onPreExecute();
        mWorker.mParams = params;
        exec.execute(mFuture);
    }
}

主要执行流程,简单地说,就是:
Task.execute -> Executor -> FutureTask -> WorkerRunnable -> Task.postResult -> Handler -> Task.finish
除去Task, 就只剩 “Executor + FutureTask + WorkerRunnable + Handler” 了。
所以,如果要分析实现,抓住流程,然后对这几个部分各个击破即可。

下面是AsyncTask的流程图:

图片出自《AsyncTask知识扫盲》,笔者做了部分补充。

3.2 任务调度

上一节我们分析执行流程,了解到AsyncTask主要是通过与“FutureTask+WorkerRunnable+Handler”密切配合,
实现了在任务执行的不同阶段,回调相应的方法
如果说这些是“框架”的话,那么Executor就是执行任务的“引擎”。
任务的调度(包括串行还是并行、并发窗口多大、如何排队……等),由Executor实现。
要理解AsyncTask的Executor,需要对线程池有一定了解,推荐阅读本系列的上一篇文章:《基础篇:速读Java线程池》

接下我们继续分析AsyncTask关于Executor的代码。
先看线程池的参数配置:

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    
    public static final Executor THREAD_POOL_EXECUTOR;
    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(128), sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

为方便分析,我们对命名做一些简化(比如用coreSize代替CORE_POOL_SIZE)。

根据线程池的特点,当一个任务提交:

如果没有任务提交,线程会在存活30s后销毁;
由于设置了allowCoreThreadTimeOut(true),即使是“核心线程”也是会销毁的。

一般情况下任务不会堆积一百多个,这时候coreSize就是线程池的“并发窗口”(同时运行的线程数)。
coreSize大小的设置其实是经过几个版本的:

注释提到,为了避免后台工作使得CPU饱和,倾向于coreSize “have 1 less than the CPU count”;
同时还加了道紧箍咒:at least 2 threads and at most 4 threads。
看得出来,SDK 的开发人员对coreSize设置多少是比较纠结的。

在线程池之外,AsyncTask还封装了一个SerialExecutor,用于任务的串行调度。
代码如下:

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);
            }
        }
    }

这是典型的装饰者模式,这种设计模式有以下特点:

  1. 装饰对象和真实对象有相同的接口,这样客户端对象就能以和真实对象相同的方式和装饰对象交互。
  2. 装饰对象包含一个真实对象的引用。
  3. 装饰对象接受所有来自客户端的请求,它把这些请求转发给真实的对象。
  4. 装饰对象可以在转发这些请求以前或以后增加一些附加功能。

SerialExecutor代码不多,却用了两次装饰者模式:Runnable和Executor。

就效果而言,AsyncTask通过最小的代价(添加SerialExecutor), 同时提供了串行和并行两种调度方式。
(此处用“并发”会更加严谨,但是说“并行”表达更加流畅一些,就先这么说了)

串行还是并行这个问题,官方文档有交代:

When first introduced, AsyncTasks were executed serially on a single background thread.
Starting with Build.VERSION_CODES.DONUT , this was changed to a pool of threads allowing multiple tasks to operate in parallel.
Starting with Build.VERSION_CODES.HONEYCOMB , 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, java.lang.Object[]) with THREAD_POOL_EXECUTOR .

简单翻译:
最初的时候,AsyncTask是串行的;
自1.6之后,改成并行了;
自3.0之后,“为避免并行导致普遍的应用程序错误”,又改成串行了;
如果确实想并行,可以调用executeOnExecutor(THREAD_POOL_EXECUTOR)。

改来改去确实不厚道,但是自从3.0之后就没有改过了,现在minSdkVersiond基本都在4.0以上了,所以算是曾经的坑吧。
最后,默认串行好还是默认并行好?SDK的人员看来也很纠结,但最终选择了串行。

这就好比建了一个游泳池,深水区和浅水区隔开,由于怕人溺水,默认开放浅水区。
如果确实想到深水区也可以,就在隔壁。

但浅水区也不是绝对安全的,比如有位开发者就遇到过这样的“坑”:
他同时用了两个SDK,一个用来做图片剪裁,一个是facebook的广告SDK。
后面发现图片加载不出来,经核查发现两个SDK都用了AsyncTask, 但都是用的串行的Executor。
国内访问外网速度偏慢,所以facebook的SDK阻塞了图片剪裁的任务。
后来作者给这个图片剪裁库的开发者提了建议,让其改用THREAD_POOL_EXECUTOR来图片剪裁,方才解了任务阻塞的问题。

串行和并行其实都是有需求的,需具体问题具体分析。

四、局限性

随着使用的深入,大家发现AsyncTask存在一些问题。
关于这些“问题”,仁者见仁,智者见智。
下面是笔者的分析:

4.1 取消任务

有的文章指出,AsyncTask的cancel()不一定起作用。
AsyncTask的cancel确实是不一定能立即取消任务,但笔者认为这其实是合理的。

4.2 内存泄漏 & 生命周期

AsyncTask若持有Activity引用,且生命周期比Activity的长,则Activity无法被及时回收。
看到这段描述,或许很多读者都能想到Handler,Handler也有此问题;其实RxJava也有这个问题。
所以这个问题不是AsyncTask独有。

解决此问题通常有两种方案:
1、声明为静态内部类,用弱引用持有Activity;
2、解决生命周期问题,随Activity销毁而销毁。

第一种方案操作成本太高,极其不方便。
如果解决生命周期问题,不单内存泄漏问题可解,很多其他的问题也会迎刃而解,可谓一石多鸟。
常见的做法就是在Activity回调函数onDestroy()中调用cancle()方法,
但是这样的话需要在Activity声明一个AsyncTask的变量,指向AsyncTask的实例,写起来也是很麻烦。

4.3 通用性

看一段API文档的描述:

AsyncTask is designed to be a helper class around Thread and Handler 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 the java.util.concurrent package such as Executor , ThreadPoolExecutor and FutureTask

Google译文:
AsyncTask旨在成为Thread和Handler的辅助类,并不构成通用的线程框架。
理想情况下,AsyncTask应该用于短操作(最多几秒钟)。
如果需要保持线程长时间运行,强烈建议您使用concurrent包提供的各种API,例如Executor,ThreadPoolExecutor和FutureTask。

简而言之就是:AsyncTask不适合执行长时间运行的任务

AsyncTask自己的实现中明明用了“Executor,ThreadPoolExecutor和FutureTask”,
却又建议别人用这些API去执行“长时间的运行”的任务,咋一看着实让人困惑。

如果读者阅读了本系列的上一篇文章,应该能理解其中原因。

首先,如果用串行的执行器,则会遇到前面提到的“任务阻塞”的问题;
然后即使使用THREAD_POOL_EXECUTOR,coreSize最大也不超过4,几个任务下来就满了,如果任务又比较耗时,后面的任务就要等很久了。
所以对于“长时间运行的任务”,THREAD_POOL_EXECUTOR 和 SERIAL_EXECUTOR 不过是五十步和百步的区别。
那为什么coreSize不设置大一点呢?
前面3.2节中有提到,“为了避免后台工作使得CPU饱和”,coreSize设置得比较小。
一方面要给UI线程保留计算资源,另一方面如果是执行的是计算密集型任务,线程数大于CPU核心,CPU利用率(用于执行任务的时间比例)反而更低,因为线程上下文切换也是有不少消耗的。

可实际上,开发中需要用到异步的,很多情况下是IO密集型操作,尤其是网络请求。
如果请求数据多,或者网络不稳定,则任务可能会“长时间运行”。
如今的APP,大量的网络请求是很常见的。
若不适用于网络请求,要之何用?

五、总结

通过前面的分析,我们了解到AsyncTask主要实现了两个方面的功能:
流程控制:任务执行的不同阶段回调相应的方法;
任务调度:串行/并行,并发控制,任务缓冲……等等。
同时,与Handler的配合,使得AsyncTask尤其适用于开发中常见的“数据加载+界面刷新”等场景,可以说是给Android量身定制的异步任务框架。

总的来说,AsyncTask构思精巧,代码简洁,用法也很简单。
但是同时AsyncTask也存在一些局限性,比如生命周期导致的内存泄漏问题,以及由于并发窗口太小而导致的通用性问题。

“夫鸡肋,弃之如可惜,食之无所得。”
AsyncTask足够的简洁,功能也不错,但通用性的缺陷极大地限制了AsyncTask的适用范围,使其显得很“鸡肋”。

那是否有办法可以方便地解决AsyncTask的生命周期问题?
是否有“双全法”,既支持CPU的高利用率,又支持任务的高吞吐率呢?

方法总比问题的多,AsyncTask其实已经迈出了很大的一步;
我们只需在其基础上再多走几步,便能突破局限,“遇见更好的AsyncTask”。

传送门:AsyncTask加强版

参考资料:
https://juejin.im/post/5a85a6066fb9a06337573955
https://cloud.tencent.com/developer/article/1328339
https://www.jianshu.com/p/94a483b4e26c
https://www.zhihu.com/question/41048032
https://www.zhihu.com/question/33515481

上一篇 下一篇

猜你喜欢

热点阅读