AsyncTask源码分析

基础知识点

1.线程池Executor
2.Future
3.Callable
4.中断线程
5.Handler
6.枚举
7.泛型
8.单例模式
9.可变参数

AsyncTask设计思想

1.回调函数
2.如何有序调度任务
3.串行或并行执行任务
4.中断任务
5.线程调度

回调函数

AsyncTask,是可异步执行任务的工具类。开发者只需要至少重写一个回调函数doInBackground(Params... params);就能完成线程调度

我们看看它的使用方法:
我们写一个抽象类AsyncTask的具体子类MyAsyncTask,并覆写其四个函数。

private class MyAsynckTask extends AsyncTask<Long, String, String> {
    /**
     * 任务被执行前,会被调用,UI线程做一些提示操作,如显示进度条
     */
    @Override protected void onPreExecute() {
      super.onPreExecute();
      Log.d(TAG, "onPreExecute: " + Thread.currentThread().getName());
    }  

    /**
     * 在后台子线程执行任务,返回类型String
     * @param params  模拟计算时间
     * @return
     */    

    @Override protected String doInBackground(Long... params) {
      try {
        TimeUnit.SECONDS.sleep(params[0]);//模拟后台耗时计算,参数大小作为计算时间
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Log.d(TAG, "doInBackground: " + Thread.currentThread().getName());
      return Thread.currentThread().getName();
    }


    /**
     * 任务执行完毕后,在主线程回调这个函数。
     * @param s  后台任务返回的结果
     */
    @Override protected void onPostExecute(String s) {
      super.onPostExecute(s);
      Log.d(TAG, "onPostExecute: " + s + " ---" + Thread.currentThread().getName());
    }

    /**
     * 任务中途被取消会回调它
     */
    @Override protected void onCancelled() {
      super.onCancelled();
      Log.d(TAG, "onCancelled: ");
    }
  }

我们主要看doInBackground(Parms...parm)函数,它是什么时候被回调的呢?在后台FutrueTask类执行call()函数时回调。 这里WorkerRunable是一个实现了Callable接口的实现类,它可以执行任务后能携带一个返回值Result,并拓展了可以持有一个Parms参数。我们看看它的回调时刻:

  public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
             /省略一些代码
            Result result = null;
            result = doInBackground(mParams);
            ````省略一些代码
            return result;
        }
  };

我们在UI线程使用它即可:

   new MyAsynckTask().execute(100l);
   new MyAsynckTask().execute(5l);
   new MyAsynckTask().execute(5l);
   new MyAsynckTask().execute(5l);

输出结果:

04-03 22:44:47.713 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:47.713 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:47.713 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:47.714 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:52.746 4256-4295/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #1
04-03 22:44:52.747 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #1 ---main
04-03 22:44:57.788 4256-4376/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #2
04-03 22:44:57.788 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #2 ---main
04-03 22:45:02.832 4256-4450/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #3
04-03 22:45:02.833 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #3 ---main
04-03 22:45:07.875 4256-4450/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #3
04-03 22:45:07.875 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #3 ---main

所以,我们在基类里面写一些函数钩子,并调用,然后然使用者在具体实现类里面去实现。

如何结合线程池串行调度任务

从上面的输出结果来看,AsyncTask是默认串行执行的(不考虑版本问题)。它用了两个调度器Executor实现。

  • SERIAL_EXECUTOR,用队列保存等待被被执行的任务
    *THREAD_POOL_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在同步方法execute(r)中的实现,它有两步:
第一步: 保存任务到队列。新建一个Runable包裹提交进来的任务,在try作用域里面先执行r.run(),必须等任务执行完后,在finally作用域中会执行 scheduleNext(),来调度下一个任务。因此任务是串行执行的。最后我们会把这个新建的Runnable对象放入队列mTasks中。

第二步:查看是否有可立即执行的任务mActive。 如果有,则立即放入THREAD_POOL_EXECUTOR执行。

我们再看看scheduleNext()函数,就是调用mTasks.poll())从队列得到下一个任务,执行。
文字描述也许会迷糊,不如看下图[采用OmniGraffle软件制作]:

串行调度图.png

至此,为什么要两个调度器呢?我认为是为了遵守单一职责的设计原则。一个调度器只负责一件事,前面SerialExecutor我们已经分析。那THREAD_POOL_EXECUTOR线程池调度器则是根据CPU数构造线程Thread缓存池,节约资源,提高AsyncTask执行性能。

3.为什么要串行执行任务?不串行可以吗?

为什么要串行执行,我认为是为了保证数据一致性。正如官方文档说的:

tasks are executed on a single thread to avoid common application errors caused by parallel execution.</p>

  • If you truly want parallel execution, you can invoke
    executeOnExecutor(java.util.concurrent.Executor, Object[])} with
  • THREAD_POOL_EXECUTOR.

意思是说,串行模式能够避免一些并发异常。如果你真的需要并发执行,这可以调用executeOnExecutor(),传入自定义一个Executor,这样就不会去调用SerialExecutor了。而我认为AsyncTask只适合执行一些小任务,因为在串行执行时,如果某个任务执行时间过长,或者中途阻塞,占有CPU太久,则会导致后面排队的任务等待过久。

4.如何取消任务

我们知道为了能得到后台线程运行后的结果,我们的任务实现了的Callable接口,它和Runnable区别是有一个返回值。现在我们想取消任务,我们可以实现Future接口,它除了能通过get()得到返回值,还有cancle()方法用来取消任务,done()回调函数监听是否任务结束

在AsyncTask中,我们用FutureTask这个系统SDK为我们实现了Ruanble和Future的实现类,它可以通过一个Callable类型的对象参数构造出来,这里使用我们的是上文中的WorkRunnable作为参数。
代码如下:

FutureTask mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };

那么如果我们想取消任务,现在是不是直接调用mFuture.cancel(true)强制中断线程就好了?答案是否定的。因为mFuture.cancel(true)方法内部实现机制其实是:

   //.....省略一些代码
   if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    U.putOrderedInt(this, STATE, INTERRUPTED);
                }
            }
   //.....省略一些代码

意思是如果任务正在运行,则interrupt()中断线程。而在这里,如果任务正好还在排队尚未执行,而我们想取消未执行的任务,那么直接调用mFuture.cancel(true)就无效!因此,我们还需要考虑未执行的任务如何中断。

令人惊喜的是,在AsyncTask中还提供了一个对象:

 private final AtomicBoolean mCancelled = new AtomicBoolean();

它是一个原子类,通过CAS机制保证了mCancelled可见性,它内部保存了当前任务是否被取消的信号。

那么正确完整的取消任务方案是分两个步骤:
第一步:调用AsyncTask中的cancel方法:

    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);//用来取消未执行的任务
        return mFuture.cancel(mayInterruptIfRunning);//用来取消正在执行的任务可能会抛出异常
    }

第二步:在doInBackground回调函数中循环去判断mCancelled中的值确认将要执行的任务是否要求取消:

 @Override protected String doInBackground(Long... params) {
      while (!isCancelled()){
        //todo something...
      }
      return null;
    }

至此,我们成功实现了取消任务功能。

线程调度

在安卓系统中,我们要从子线程更新UI主线程,当然是利用handler+looper+messageQueue机制,在AsyncTask中不例外也是如此。但是除此之外,我们可以学习如何通过单例模式构造一个handler,如何强制构造一个主线程执行的handler。
1.单例模式:

 private static Handler getHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler();
            }
            return sHandler;
        }
    }

看看源码中的这个单例模式,比较简陋,也不是什么double-check :)

2.强制在主线程执行handler

 private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }
}

如上在父类构造函数 [super(Looper.getMainLooper())] 初始化Looper。

执行流程图

总调度图.png

总结

AsyncTask作为Activity内部类使用的时候还会因为持有acitivity对象引用,在后台导致内存泄漏问题。推荐阅读其他执行异步任务类框架, 如Job、Rxjava等。

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

推荐阅读更多精彩内容

  • 使用AsyncTask的一般步骤是: 定义一个类继承自AsyncTask,实现抽象方法 new 一个AsyncTa...
    yk_looper阅读 374评论 0 2
  • 转载请注明出处:http://www.jianshu.com/p/531657db36f4 上一篇主要说了下Asy...
    朋永阅读 270评论 0 0
  • 在Android开发道路上,有一个类你是无论如何都无法绕过去的。那就是AsyncTask,因为使用的足够简单,在面...
    人失格阅读 556评论 1 3
  • 元亨,利贞;勿用有攸往,利建侯。 学习了建构主义之后,接触到任何好知识,都想着如何转变成可以让大家都能掌握的课程,...
    承谦阅读 348评论 0 1
  • 缘起 可能只是偶然,可能是想了解下王兴这个人。 英语representation讲公司或创始人文化时,有人讲了王兴...
    im天行阅读 537评论 0 1