AsyncTask:一只命途多舛的小麻雀

麻雀虽小

AsyncTask是一只命途多舛的小麻雀,为什么说它命途多舛,因为它一直被反复折腾,从Android 1.6之前,然后1.6到2.3,再从3.0到现在(其实5.1开始后也有细微的改动),反反复复,从串行到并行,再恢复至串行,可见其内心之纠结,尽管如此,它还是不断被开发人员各种吐槽,内存泄露,不靠谱的cancel等等,真可谓命途多舛。

可是天将降大任于斯人也,它毕竟是一只小麻雀啊,不到三百行代码量的一只小麻雀,其特点便是麻雀虽小五脏俱全,因此好好解读一下源码是很有必要的。如何来解读呢,不妨我们自己来创建一只简化版的小麻雀——SimpleAsyncTask,通过这种方式来加深理解。

作为一个标题党,小麻雀已经完成它的使命了,其实这篇文章真正的标题应该是《从FutureTask到AsyncTask》或者《自己写个AsyncTask》,以下,开始正题。

一 需求和api选型

想想看我们对于AsyncTask的需求是什么,大概会有以下几点:

  1. 能定义以下执行过程的操作:预执行、执行后台任务、执行进度反馈、执行完毕、终止线程执行
  2. 能停止线程任务
  3. 子线程能与UI线程进行通讯(反馈线程执行进度和执行结果)

第1点,我们定义这些方法目的是让调用者重写而实现各种交互,因此这5种操作实际上是5个抽象方法(或者空方法),我们需要在SimpleAsyncTask类中合适的时机去调用这5个抽象方法,显然,这是模板方法模式(Template Method)的应用。

对于第2点,如何停止线程?这方面的最佳实践是中断+条件变量,纵观java.util.concurrent,有个api很适合这个场景,那便是Future和Callback,而作为Future的唯一实现——FutureTask便是我们写AsyncTask的重点。

第3点,看到子线程和UI线程,可以想到的方案便是 Handler,关于Handler,请参考笔者写的《Handler和他的小伙伴们》上。 有一点要注意的是,AsyncTask会在各Activity中实例化,有可能在主线程或子线程中实例化,这么多的AsyncTask实例中,我们只需要一个持有mainLooper的Handler的实例,因此它将是一个单例对象,单个进程内共享

确定好需求和api选型之后,接下来我们来写代码。

二 使用Template Method

首先定义五个空方法:预执行、执行后台任务、执行进度反馈、执行完毕、终止线程执行。

public abstract class SimpleAsyncTask<Params, Progress, Result> {

    /**
     * 模板方法1-预执行,应在线程启动之前执行
     */
    protected void onPreExecute() {
    }

    /**
     * 模板方法2-执行后台任务,应该在线程体中调用,即在FutureTask中调用
     */
    protected abstract Result doInBackground(Params params);

    /**
     * 模板方法3-执行进度反馈,应该在Handler中调用
     */
    protected void onProgressUpdate(Progress progress) {
    }

    /**
     * 模板方法4-执行完毕,应该在Handler中调用
     */
    protected void onPostExecute(Result result) {
    }

    /**
     * 模板方法5-终止线程执行,应该在Handler中调用
     */
    protected void onCancelled() {
    }
}

这几个方法见名知意,同时从各自携带的参数类型可以清晰的理解几个泛型的作用:

  • Params:执行后台任务的时候使用
  • Progress:反馈进度的时候使用
  • Result:反馈结果的时候使用
    (注:这里为了代码简洁,去掉了Params和Progress的可变参数)

当然,模板方法模式最重要的是我们在何时去调用这些抽象的模板方法,很显然,doInBackground作为后台执行的逻辑,应该在线程中调用,也就是下文会讲到的FutureTask,而其他几个方法跟UI有关系,会在主线程中执行,因此它们会在Handler中调用到。

三 FutureTask开发

上文提到,Future和Callable是java.util.concurrent中提供了取消任务的一组api,要理解AsyncTask的话,需要先理解下这对组合。以下做个简单的介绍:

  1. Callable与Runable
public interface Callable<V> {
    V call() throws Exception;
}

简单来讲,Callable接口等价于Runable,call()等价于run(),区别在于它是有返回值的。

我们可以通过ExecutorService调用Callable,执行后将返回Future对象,比如:
Future<String> future = Executors.newSingleThreadExecutor().submit(mCallable);

  1. Future
public interface Future<V> {
   boolean cancel(boolean mayInterruptIfRunning);

   boolean isDone();
 
   V get() throws InterruptedException, ExecutionException;

   V get(long timeout, TimeUnit unit)
       throws InterruptedException, ExecutionException, TimeoutException;
}

Future接口两个方法着重理解下,一是cancel(boolean mayInterruptIfRunning),顾名思义就是终止线程,二是get(),它会阻塞线程,直到Callable的call()返回对象,并以此作为返回值。至于mayInterruptIfRunning这个boolean值的含义,大家看看FutureTask中相应的源码就直到了,其实只是多了thread.interrupt()的逻辑而已。结合Callable的代码,Future的使用如下:

Future<String> future = Executors.newSingleThreadExecutor().submit(mCallable);
//阻塞线程,等待Callable.call()的返回值
String result = future.get();
  1. FutureTask

FutureTask的继承关系

从FutureTask的继承关系来看,它既是Runable也是Future,所以我们可以把当做Runable来使用,同时它也具备Future的能力,可以终止线程,可以阻塞线程,等待Callable的执行,并获取返回值。另外要注意的是,它的构造函数是public FutureTask(Callable<V> callable),因此实例化FutureTask时需要Callable对象作为参数。

关于这部分基础知识的demo代码在此处,有需要的可以跑起来看看。

  1. SimpleAsyncTask中的FutureTask
    介绍完Future和Callable的基础知识后,我们回归正题。FutureTask在AsyncTask里充当了线程的角色,因此耗时的后台任务doInBackground应该在FutureTask中调用,同时我们还要提供线程池对象来执行FutureTask,代码如下:
public abstract class SimpleAsyncTask<Params, Progress, Result> {
    
    //...省略部分代码
    private static final Executor EXECUTOR = Executors.newCachedThreadPool();
    private WorkerRunnable<Params, Result> mWorker;
    private FutureTask<Result> mFuture;

    public SimpleAsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            @Override
            public Result call() throws Exception {
                //调用模板方法2-执行后台任务
                Result result = doInBackground(mParams);
                //提交结果给Handler
                return postResult(result);
            }
        };

        //此为线程对象
        mFuture = new FutureTask<>(mWorker);
    }

    public void execute(Params params) {
        mWorker.mParams = params;
        //在线程启动前调用预执行的模板方法,意味着它在调用AsyncTask.execute()的所在线程里执行,如果是在子线程中,则无法处理UI
        //调用模板方法1-预执行
        onPreExecute();
        //执行FutureTask启动线程
        EXECUTOR.execute(mFuture);
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params mParams;
    }
    //...省略部分代码
}

四 Handler开发

到目前为止,我们已经消费掉了两个模板方法,分别是onPreExecute和doInBackground,此时还剩下3个模板方法,他们都需要有UI交互的,因此他们将在Handler中被调用。

首先,对于终止线程和线程执行完毕这两个方法,我们都称之为线程finish了,所以我们先定义个finish(Result result)方法,如下:

private void finish(Result result) {
    if (isCancelled()) {
        //调用模板方法:终止线程执行
        onCancelled();
    } else {
        //调用模板方法:线程执行完毕
        onPostExecute(result);
    }
}

接下来,开始写关键的Handler类对象,因为该Handler位于SimpleAsyncTask内部,因此把它命名为InternalHandler,这个静态内部类有几点要注意:

  1. 它是静态的单实例,所有AsyncTask对象共享
  2. 它必须持有mainLooper对象,才能与主线程进行交互
  3. 注意它调用几个模板方法的时机
    代码如下:
public abstract class SimpleAsyncTask<Params, Progress, Result> {

    //省略部分代码
    
    /**
     * 一个静态的单例对象,所有AsyncTask对象共享的
     */
    private static InternalHandler sHandler = new InternalHandler();
    
    private static class InternalHandler extends Handler {
        /**
         * 注:此为android 22之后的写法,构造函数里默认指定mainLooper对象
         * 它的好处是无论Handler在什么时机被实例化,都可以与主线程进行交互
         * 相比之下,之前版本的AsyncTask必须在ActivityThread中执行AsyncTask.init()
         */
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    //调用模板方法4和5,取消或者完成
                    result.mTask.finish(result.mData);
                    break;
                case MESSAGE_POST_PROGRESS:
                    //调用模板方法3-执行进度反馈
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
    
    /**
     * 由于InternalHandler是静态内部类,无法引用外部类SimpleAsyncTask的实例对象,
     * 因此需要将外部类对象作为属性传递进来,所以封装此类
     */
    private static class AsyncTaskResult<Data> {
        final SimpleAsyncTask mTask;
        final Data mData;

        AsyncTaskResult(SimpleAsyncTask task, Data data) {
            mTask = task;
            mData = data;
        }
    }

    //省略部分代码
}

到目前为止5个模板方法都已经调用到了,其中四个方法均有触发的时机和调用的时机,除了onProgressUpdate,它只有调用,但并没有触发的时机,因此,我们还要提供一个方法,供调用者主动触发:

protected final void publishProgress(Progress progress) {
    if (!isCancelled()) {
        AsyncTaskResult<Progress> taskResult = new AsyncTaskResult<>(this, progress);
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS, taskResult).sendToTarget();
    }
}

写到这里,我们这只简化版的小麻雀基本上已经完成,当然对比源码还是有一点点细微的不同,大家可以自行对比一下。通过自己写个SimpleAsyncTask的方式可以帮助我们更好的理解源码,以上所写的完整代码在此

五 关于其他细节

  • 串行or并行?
    在SimpleAsyncTask中,我们使用private static final Executor EXECUTOR = Executors.newCachedThreadPool()作为线程池,而实际上,源码中的默认线程池是自定义的,这个类是SerialExecutor,从类的命名上看,Serial是串行的意思,所以很明显,AsyncTask默认是串行的。除此之外,AsyncTask里还有个线程池 THREAD_POOL_EXECUTOR,实在需要并行的话我们就用这个线程池。

如果都些都不满足要求,我们也可以自定义符合自己业务要求的线程池,并通过setDefaultExecutor(Executor exec)改变默认的线程池。

  • 不能执行多次
    AsyncTask只能执行一次,类似这样的代码是会抛异常的:
MyAsyncTask asyncTask = new MyAsyncTask();
asyncTask.execute();
asyncTask.execute();

原理很简单,AsyncTask会判断当前状态,如果是RUNNING或者FINISHED状态,则直接抛异常:

//execute最终会执行到executeOnExecutor
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)");
       }
   }
   //省略部分代码
}

  • AsyncTask是否只能在主线程创建和运行?
    比如,这样的代码能否正常使用:
new Thread(new Runnable() {
    @Override
    public void run() {
        MyAsyncTask myAsyncTask = new MyAsyncTask();
        mSimpleAsyncTask.execute("task1");
    }
}).start();

从Android4.1(API 16)之后其实已经没什么问题了,通过源码来理解的话非常简单,比如在我们自己写的SimpleAsyncTask中,重写了Handler的构造器,如下:

public InternalHandler() {
    super(Looper.getMainLooper());
}

这样一来,无论AsyncTask在什么时候创建,实例化出来的静态InternalHandler对象都持有mainLooper,都能与主线程进行通讯。有一点小区别,如果在子线程中调用execute(),则onPreExecute不能执行UI的操作,否则会抛异常。

要注意的是,这是Android 5.1(API 22)以及之后的写法,API 16~API 21是另外一种写法,参考下一点。

  • AsyncTask.init()
    AsyncTask中有个隐藏方法init()(API 22之后已经移除)
/** @hide Used to force static handler to be created. */
public static void init() {
    sHandler.getLooper();
}

与此对应的,在ActivityThread的main方法中会调用AsyncTask.init(),目的是什么?

由于AsyncTask中的Handler是静态的单实例对象,他会在类加载期间进行初始化,万一调用者在子线程中加载AsyncTask,将会导致同一进程的所有AsyncTask无法使用。因此,系统先下手为强,一开始就直接加载,保证该Handler持有主线程的mainLooper,能正常进行UI交互。

  • postResultIfNotInvoked的作用是什么?
    AsyncTask有很多逻辑干扰了我们解读源码,postResultIfNotInvoked便是其中一个。它实际上是Google解决的一个bug,确保如果cancel()方法过早调用的场景下,onCancelled()仍然能顺利的执行,参考stackoverflow这篇文章

比如,我们自定义的SimpleAsyncTask,如果我们执行以下代码,onCancelled()是不会执行的,而AsyncTask则可以正常执行:

mSimpleAsyncTask = new MySimpleAsyncTask();
mSimpleAsyncTask.execute("task1");
//马上终止线程
mSimpleAsyncTask.cancel(true);

六 最后

AsyncTask作为一只命途多舛的小麻雀,他是一只纯粹的麻雀,脱离了低级趣味的麻雀,值得好好品尝的小麻雀。

参考文章:
http://blog.csdn.net/guolin_blog/article/details/11711405
http://droidyue.com/blog/2015/12/20/worker-thread-in-android/?droid_refer=ninki_posts
http://droidyue.com/blog/2014/11/08/bad-smell-of-asynctask-in-android/index.html
http://stackoverflow.com/questions/25322651/asynctask-source-code-questions

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容