AsyncTask:一只命途多舛的小麻雀
LeonorStack
8年前
<p style="text-align: center;"><img src="https://simg.open-open.com/show/2a0b5c16965d130221b534577340afa2.jpg" alt="AsyncTask:一只命途多舛的小麻雀" width="663" height="480"></p> <p style="text-align: center;">麻雀虽小</p> <p><br> AsyncTask是一只命途多舛的小麻雀,为什么说它命途多舛,因为它一直被改,从Android 1.6之前,然后1.6到2.3,再从3.0到现在(其实5.1开始后也有细微的改动),反反复复,从串行到并行,再恢复至串行,可见其内心之纠结,尽管如此,它还是不断被开发人员各种吐槽,内存泄露,不靠谱的cancel等等,真可谓命途多舛。</p> <p>可是天将降大任于斯人也,它毕竟是一只小麻雀啊,不到三百行代码量的一只小麻雀,其特点便是麻雀虽小五脏俱全,因此好好解读一下源码是很有必要的。如何来解读呢,不妨我们自己来创建一只简化版的小麻雀——SimpleAsyncTask,通过这种方式来加深理解。</p> <p>作为一个标题党,小麻雀已经完成它的使命了,其实这篇文章真正的标题应该是《从FutureTask到AsyncTask》或者《自己写个AsyncTask》,以下,开始正题。</p> <p>一 需求和api选型</p> <p>想想看我们对于AsyncTask的需求是什么,大概会有以下几点:</p> <ol> <li>能定义以下执行过程的操作:预执行、执行后台任务、执行进度反馈、执行完毕、终止线程执行</li> <li>能停止线程任务</li> <li>子线程能与UI线程交互(反馈线程执行进度和执行结果)</li> </ol> <p>第1点,我们定义这些方法目的是让调用者重写而实现各种交互,因此这5种操作实际上是5个抽象方法(或者空方法),我们需要在SimpleAsyncTask类中合适的时机去调用这5个抽象方法,显然,这是模板方法模式(Template Method)的应用。</p> <p>对于第2点,如何停止线程?这方面的最佳实践是中断+条件变量,纵观java.util.concurrent,有个api很适合这个场景,那便是Future和Callback,而作为Future的唯一实现——FutureTask便是我们写AsyncTask的重点。</p> <p>第3点,看到子线程和UI线程,so easy呀,有了Handler,麻麻再也不用担心线程之间的通讯了,关于Handler,如果你闲的蛋疼的话不妨看看笔者写的<a href="/misc/goto?guid=4958967343589762124">《Handler和他的小伙伴们》上</a>和<a href="/misc/goto?guid=4958967343706154114">中</a>。 有一点要注意的是,AsyncTask会在各Activity中实例化,有可能在主线程或子线程中实例化,这么多的AsyncTask实例中,<strong>我们只需要一个持有mainLooper的Handler的实例,因此它将是一个单例对象,单个进程内共享</strong>。</p> <p>确定好需求和api选型之后,接下来我们来写代码。</p> <p>二 使用Template Method</p> <p>首先定义五个空方法:预执行、执行后台任务、执行进度反馈、执行完毕、终止线程执行。</p> <pre> <code class="language-java">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() { } }</code></pre> <p>这几个方法见名知意,同时从各自携带的参数类型可以清晰的理解几个泛型的作用:</p> <ul> <li>Params:执行后台任务的时候使用</li> <li>Progress:反馈进度的时候使用</li> <li>Result:反馈结果的时候使用<br> (注:这里为了代码简洁,去掉了Params和Progress的可变参数)</li> </ul> <p>当然,模板方法模式最重要的是我们在何时去调用这些抽象的模板方法,很显然,doInBackground作为后台执行的逻辑,应该在线程中调用,也就是下文会讲到的FutureTask,而其他几个方法跟UI有关系,会在主线程中执行,因此它们会在Handler中调用到。</p> <p>三 FutureTask开发</p> <p>上文提到,Future和Callable是java.util.concurrent中提供了取消任务的一组api,要理解AsyncTask的话,需要先理解下这对组合。以下做个简单的介绍:</p> <ol> <li> <p>Callable与Runable</p> <pre> <code class="language-java">public interface Callable<V> { V call() throws Exception; }</code></pre> <p>简单来讲,Callable接口等价于Runable,call()等价于run(),区别在于它是有返回值的。</p> <p>我们可以通过ExecutorService调用Callable,执行后将返回Future对象,比如:</p> <br> <code>Future<String> future = Executors.newSingleThreadExecutor().submit(mCallable);</code></li> <li> <p>Future</p> <pre> <code class="language-java">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; }</code></pre> <p>Future接口两个方法着重理解下,一是<code>cancel(boolean mayInterruptIfRunning)</code>,顾名思义就是终止线程,二是<code>get()</code>,它会阻塞线程,直到Callable的call()返回对象,并以此作为返回值。至于<code>mayInterruptIfRunning</code>这个boolean值的含义,大家看看FutureTask中相应的源码就直到了,其实只是多了<code>thread.interrupt()</code>的逻辑而已。结合Callable的代码,Future的使用如下:</p> <pre> <code class="language-java">Future<String> future = Executors.newSingleThreadExecutor().submit(mCallable); //阻塞线程,等待Callable.call()的返回值 String result = future.get();</code></pre> </li> <li> <p>FutureTask</p> <img src="https://simg.open-open.com/show/f20952e62566b95d0725f13f0403bb14.png" alt="AsyncTask:一只命途多舛的小麻雀" width="391" height="313"> <p>FutureTask的继承关系</p> <p><br> 从FutureTask的继承关系来看,它既是Runable也是Future,所以我们可以把当做Runable来使用,同时它也具备Future的能力,可以终止线程,可以阻塞线程,等待Callable的执行,并获取返回值。另外要注意的是,它的构造函数是<code>public FutureTask(Callable<V> callable)</code>,因此实例化FutureTask时需要Callable对象作为参数。</p> <p>关于这部分基础知识的<a href="/misc/goto?guid=4959671753584274749">demo代码在此处</a>,有需要的可以跑起来看看。</p> </li> <li> <p>SimpleAsyncTask中的FutureTask<br> 介绍完Future和Callable的基础知识后,我们回归正题。FutureTask在AsyncTask里充当了线程的角色,因此耗时的后台任务doInBackground应该在FutureTask中调用,同时我们还要提供线程池对象来执行FutureTask,代码如下:</p> <pre> <code class="language-java">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; } //...省略部分代码 }</code></pre> </li> </ol> <p>四 Handler开发</p> <p>到目前为止,我们已经消费掉了两个模板方法,分别是onPreExecute和doInBackground,此时还剩下3个模板方法,他们都需要有UI交互的,因此他们将在Handler中被调用。</p> <p>首先,对于终止线程和线程执行完毕这两个方法,我们都称之为线程finish了,所以我们先定义个<code>finish(Result result)</code>方法,如下:</p> <pre> <code class="language-java">private void finish(Result result) { if (isCancelled()) { //调用模板方法:终止线程执行 onCancelled(); } else { //调用模板方法:线程执行完毕 onPostExecute(result); } }</code></pre> <p>接下来,开始写关键的Handler类对象,因为该Handler位于SimpleAsyncTask内部,因此把它命名为InternalHandler,这个静态内部类有几点要注意:</p> <ol> <li>它是静态的单实例,所有AsyncTask对象共享</li> <li>它必须持有mainLooper对象,才能与主线程进行交互</li> <li> <p>注意它调用几个模板方法的时机<br> 代码如下:</p> <pre> <code class="language-java">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; } } //省略部分代码 }</code></pre> <p>到目前为止5个模板方法都已经调用到了,其中四个方法均有触发的时机和调用的时机,除了onProgressUpdate,它只有调用,但并没有触发的时机,因此,我们还要提供一个方法,供调用者主动触发:</p> <pre> <code class="language-java">protected final void publishProgress(Progress progress) { if (!isCancelled()) { AsyncTaskResult<Progress> taskResult = new AsyncTaskResult<>(this, progress); sHandler.obtainMessage(MESSAGE_POST_PROGRESS, taskResult).sendToTarget(); } }</code></pre> <p>写到这里,我们这只简化版的小麻雀基本上已经完成,当然对比源码还是有一点点细微的不同,大家可以自行对比一下。通过自己写个SimpleAsyncTask的方式可以帮助我们更好的理解源码,<a href="/misc/goto?guid=4959671753684123633">以上所写的完整代码在此</a>。</p> </li> </ol> <p>五 关于其他细节</p> <ul> <li> <p>串行or并行?<br> 在SimpleAsyncTask中,我们使用</p> <code>private static final Executor EXECUTOR = Executors.newCachedThreadPool()</code>作为线程池,而实际上,源码中的默认线程池是自定义的,这个类是<code>SerialExecutor</code>,从类的命名上看,Serial是串行的意思,所以很明显,AsyncTask默认是串行的。除此之外,AsyncTask里还有个线程池 <code>THREAD_POOL_EXECUTOR</code>,实在需要并行的话我们就用这个线程池。 <p>如果都些都不满足要求,我们也可以自定义符合自己业务要求的线程池,并通过<code>setDefaultExecutor(Executor exec)</code>改变默认的线程池。</p> </li> <li> <p>不能执行多次<br> AsyncTask只能执行一次,类似这样的代码是会抛异常的:</p> <pre> <code class="language-java">MyAsyncTask asyncTask = new MyAsyncTask(); asyncTask.execute(); asyncTask.execute();</code></pre> <p>原理很简单,AsyncTask会判断当前状态,如果是RUNNING或者FINISHED状态,则直接抛异常:</p> <pre> <code class="language-java">//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)"); } } //省略部分代码 }</code></pre> </li> <li> <p>AsyncTask是否只能在主线程创建和运行?<br> 比如,这样的代码能否正常使用:</p> <pre> <code class="language-java">new Thread(new Runnable() { @Override public void run() { MyAsyncTask myAsyncTask = new MyAsyncTask(); mSimpleAsyncTask.execute("task1"); } }).start();</code></pre> <p>从Android4.1(API 16)之后其实已经没什么问题了,通过源码来理解的话非常简单,比如在我们自己写的SimpleAsyncTask中,重写了Handler的构造器,如下:</p> <pre> <code class="language-java">public InternalHandler() { super(Looper.getMainLooper()); }</code></pre> <p>这样一来,无论AsyncTask在什么时候创建,实例化出来的静态InternalHandler对象都持有mainLooper,都能与主线程进行通讯。有一点小区别,如果在子线程中调用execute(),则onPreExecute不能执行UI的操作,否则会抛异常。</p> <p>要注意的是,这是Android 5.1(API 22)以及之后的写法,API 16~API 21是另外一种写法,参考下一点。</p> </li> <li> <p>AsyncTask.init()<br> AsyncTask中有个隐藏方法init()(API 22之后已经移除)</p> <pre> <code class="language-java">/** @hide Used to force static handler to be created. */ public static void init() { sHandler.getLooper(); }</code></pre> <p>与此对应的,在ActivityThread的main方法中会调用AsyncTask.init(),目的是什么?</p> <p>由于AsyncTask中的Handler是静态的单实例对象,他会在类加载期间进行初始化,万一调用者在子线程中加载AsyncTask,将会导致同一进程的所有AsyncTask无法使用。因此,系统先下手为强,一开始就直接加载,保证该Handler持有主线程的mainLooper,能正常进行UI交互。</p> </li> <li> <p>postResultIfNotInvoked的作用是什么?<br> AsyncTask有很多逻辑干扰了我们解读源码,postResultIfNotInvoked便是其中一个。它实际上是Google解决的一个bug,确保如果cancel()方法过早调用的场景下,onCancelled()仍然能顺利的执行,参考<a href="/misc/goto?guid=4959671753774739060">stackoverflow这篇文章</a>。</p> <p>比如,我们自定义的SimpleAsyncTask,如果我们执行以下代码,onCancelled()是不会执行的,而AsyncTask则可以正常执行:</p> <pre> <code class="language-java">mSimpleAsyncTask = new MySimpleAsyncTask(); mSimpleAsyncTask.execute("task1"); //马上终止线程 mSimpleAsyncTask.cancel(true);</code></pre> </li> </ul> <p>六 最后</p> <p>AsyncTask作为一只命途多舛的小麻雀,他是一只纯粹的麻雀,脱离了低级趣味的麻雀,值得好好品尝的小麻雀。</p> <p>参考文章:<br> <a href="/misc/goto?guid=4958837202571753213">http://blog.csdn.net/guolin_blog/article/details/11711405</a><br> <a href="/misc/goto?guid=4959671753893645086">http://droidyue.com/blog/2015/12/20/worker-thread-in-android/?droid_refer=ninki_posts</a><br> <a href="/misc/goto?guid=4959671753986971116">http://droidyue.com/blog/2014/11/08/bad-smell-of-asynctask-in-android/index.html</a><br> <a href="/misc/goto?guid=4959671753774739060">http://stackoverflow.com/questions/25322651/asynctask-source-code-questions</a></p> <p><br> </p> <p><a href="/misc/goto?guid=4959671754082152622">文/geniusmart(简书作者)</a><br> </p>