微信号:BetterAndroid

介绍:从Framework层原理到应用层开发的最佳实践,充分考虑碎片化阅读的特点,让您在等地铁的时间就能学会写出更优秀的app.

从源码的角度认识AsyncTask

2016-06-18 00:07 absfree

一、为什么需要工作者线程

    我们知道,Android应用的主线程(UI 线程)肩负着绘制用户界面和及时响应用户操作的重任,为了避免“用户点击按钮后没反应”这样的糟糕用户体验,我们就要确保主线程时刻保持着较高的响应性。为了做到这一点,我们就要把耗时的任务移出主线程,那么耗时的任务交给谁来完成呢?答案就是工作者线程。Android开发中我们通常让主线程负责前台用户界面的绘制以及响应用户的操作,让工作者线程在后台执行一些比较耗时的任务。Android中的工作者线程主要有AsyncTask、IntentService、HandlerThread,它们本质上都是对线程或线程池的封装。对线程和线程池还不太熟悉的小伙伴请参考文末“延伸阅读”部分给出的链接。

    总的来说,我们使用工作者线程是因为主线程已经有很多活要干了,累活就得交给别人干。AsyncTask是我们日常中广泛使用的一种工作者线程,它的方便之处在于可以在后台任务执行完毕时根据返回结果相应的更新UI。下面我们来研究一下它的工作原理。

 

二、探索AsyncTask的工作原理

1. AsyncTask的使用简介

    AsyncTask是对Handler与线程池的封装。使用它的方便之处在于能够更新用户界面,当然这里更新用户界面的操作还是在主线程中完成的,但是由于AsyncTask内部包含一个Handler,所以可以发送消息给主线程让它更新UI。另外,AsyncTask内还包含了一个线程池。使用线程池的主要原因是避免不必要的创建及销毁线程的开销。设想下面这样一个场景:有100个只需要0.1ms就能执行完毕的任务,如果创建100个线程来执行这些任务,执行完任务的线程就进行销毁。那么创建与销毁进程的开销就很可能成为了影响性能的瓶颈。通过使用线程池,我们可以实现维护固定数量的线程,不管有多少任务,我们都始终让线程池中的线程轮番上阵,这样就避免了不必要的开销。‘

    在这里简单介绍下AsyncTask的使用方法,为后文对它的工作原理的研究做铺垫,关于AsyncTask的详细介绍大家可以参考官方文档或是相关博文。

    AsyncTask是一个抽象类,我们在使用时需要定义一个它的派生类并重写相关方法。AsyncTask类的声明如下:

public abstract class AsyncTask<Params, Progress, Result> 

    

    我们可以看到,AsyncTask是一个泛型类,它的三个类型参数的含义如下:

  • Params:doInBackground方法的参数类型;

  • Progress:AsyncTask所执行的后台任务的进度类型;

  • Result:后台任务的返回结果类型。

    我们再来看一下AsyncTask类主要为我们提供了哪些方法:

onPreExecute() //此方法会在后台任务执行前被调用,用于进行一些准备工作
doInBackground(Params... params) //此方法中定义要执行的后台任务,在这个方法中可以调用publishProgress来更新任务进度(publishProgress内部会调用onProgressUpdate方法)
onProgressUpdate(Progress... values) //由publishProgress内部调用,表示任务进度更新
onPostExecute(Result result) //后台任务执行完毕后,此方法会被调用,参数即为后台任务的返回结果
onCancelled() //此方法会在后台任务被取消时被调用

    

    以上方法中,除了doInBackground方法由AsyncTask内部线程池执行外,其余方法均在主线程中执行。

2. AsyncTask的局限性

    AsyncTask的优点在于执行完后台任务后可以很方便的更新UI,然而使用它存在着诸多的限制。先抛开内存泄漏问题,使用AsyncTask主要存在以下局限性:

  • 在Android 4.1版本之前,AsyncTask类必须在主线程中加载,这意味着对AsyncTask类的第一次访问必须发生在主线程中;在Android 4.1以及以上版本则不存在这一限制,因为ActivityThread(代表了主线程)的main方法中会自动加载AsyncTask

  • AsyncTask对象必须在主线程中创建

  • AsyncTask对象的execute方法必须在主线程中调用

  • 一个AsyncTask对象只能调用一次execute方法

    接下来,我们从源码的角度去探究一下AsyncTask的工作原理,并尝试着搞清楚为什么会存在以上局限性。

3. AsyncTask的工作原理

    首先,让我们来看一下AsyncTask类的构造器都做了些什么:


public AsyncTask() {
 mWorker = new WorkerRunnable<Params, Result>() {
   public Result call() throws Exception {
     mTaskInvoked.set(true);
     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
     //noinspection unchecked
Result result = doInBackground(mParams);
     Binder.flushPendingCommands();
     return postResult(result);
   }
 };
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);
     }
   }
 };
}


    以上代码中,首先初始化了mWorker,它是一个派生自WorkRunnable类的对象。WorkRunnable是一个抽象类,它实现了Callable<Result>接口。我们再来看一下call方法的定义,首先将mTaskInvoked设为true表示当前任务已被调用过,然后通过Process.setThreadPriority方法设置线程的优先级。接着会调用AsyncTask对象的doInBackground方法开始执行我们所定义的后台任务,并获取返回结果存入result中。最后将任务返回结果传递给postResult方法。关于postResult方法我们会在下文进行分析。由此我们可以知道,实际上AsyncTask的成员mWorker包含了AyncTask最终要执行的任务(即mWorker的call方法)。

    接下来让我们看看对mFuture的初始化。我们可以看到mFuture是一个FutureTask的直接子类(匿名内部类)的对象,在FutureTask的构造方法中我们传入了mWorker作为参数。我们使用的是FutureTask的这个构造方法:


public FutureTask(Callable<V> callable) {
 if (callable == null) throw new NullPointerException();
 this.callable = callable;
 this.state = NEW; // ensure visibility of callable
}


   也就是说,mFuture是一个封装了我们的后台任务的FutureTask对象,FutureTask类实现了FutureRunnable接口,通过这个接口可以方便的取消后台任务以及获取后台任务的执行结果。想要进一步了解,请参考“延伸阅读”部分给出的链接。

    从上面的分析我们知道了,当mWorker中定义的call方法被执行时,doInBackground就会开始执行,我们定义的后台任务也就真正开始了。那么这个call方法什么时候会被调用呢?我们可以看到经过层层封装,实际上是mFuture对象封装了call方法,当mFuture对象被提交到AsyncTask包含的线程池执行时,call方法就会被调用,我们定义的后台任务也就开始执行了。下面我们来看一下mFuture是什么时候被提交到线程池执行的。

   

    首先来看一下execute方法的源码:

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


     我们可以看到它接收的参数是Params类型的参数,这个参数会一路传递到doInBackground方法中。execute方法仅仅是调用了executeOnExecutor方法,并将executeOnExecutor方法的返回值作为自己的返回值。我们注意到,传入了sDefaultExecutor作为executeOnExecutor方法的参数,那么sDefaultExecutor是什么呢?简单的说,它是AsyncTask的默认执行器(线程池)。AsyncTask可以以串行(一个接一个的执行)或并行(一并执行)两种方式来执行后台任务,在Android3.0及以后的版本中,默认的执行方式是串行。这个sDefaultExecutor就代表了默认的串行执行器(线程池)。也就是说我们平常在AsyncTask对象上调用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)");
   }
 }
 mStatus = Status.RUNNING;
 onPreExecute();
 mWorker.mParams = params;
 exec.execute(mFuture);
 return this;
}


     从以上代码中我们可以知道,当AsyncTask对象的当前状态为RUNNING或FINISHED时,调用execute方法会抛出异常,这意味着不能对正在执行任务的AsyncTask对象或是已经执行完任务的AsyncTask对象调用execute方法,这也就解释了我们上面提到的局限中的最后一条。

    接着我们看到上面的代码中存在一个对onPreExecute方法的调用,这表示了在执行后台任务前确实会调用onPreExecute方法。

    接着往下看,我们传入的execute方法的params参数赋值给了mWorker的mParams成员变量;而后在调用了exec的execute方法,并传入了mFuture作为参数。exec就是我们传进来的sDefaultExecutor。那么接下来我们看看sDefaultExecutor究竟是什么。在AsyncTask类的源码中,我们可以看到这句:


private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;


     sDefaultExecutor被赋值为SERIAL_EXECUTOR,那么我们来看一下SERIAL_EXECUTOR:

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

 

    现在,我们知道了实际上sDefaultExecutor是一个SerialExecutor对象,我们来看一下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);
   }
 }
}


     我们来看一下execute方法的实现。mTasks代表了SerialExecutor这个串行线程池的任务缓存队列,首先,我们用offer方法向任务缓存队列中添加一个任务,任务的内容如接下来的的run方法定义所示。我们可以看到,run方法中:先是调用了mFuture(execute方法的参数r就是我们传入的mFuture)的run方法,而mFuture的run方法内部会调用mWorker的call方法,然后就会调用doInBackground方法,我们的后台任务也就开始执行了。那么我们提交到任务缓存队列中的任务什么时候会被执行呢?我们接着往下看。

     首先我们看到SerialExecutor类中定义了一个Runnable变量mActive,它代表了当前正在执行的AsyncTask对象。execute方法中会判断mActive是否为null,若为null,就调用scheduleNext方法。在scheduleNext方法中,若缓存队列非空,则调用THREAD_POOL_EXECUTOR.execute方法执行从缓存队列中取出的任务,这时我们的后台任务便开始你真正执行了。

     通过以上的分析,我们可以知道SerialExecutor所完成的工作主要是把任务加到任务缓存队列中,而真正执行任务的是THREAD_POOL_EXECUTOR。我们来看下THREAD_POOL_EXECUTOR是什么:


public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

 

   从上面的代码我们可以知道,它是一个线程池对象。根据AsyncTask的源码,我们可以获取它的各项参数如下:


private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
 private final AtomicInteger mCount = new AtomicInteger(1);
 public Thread newThread(Runnable r) {
   return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
 }
};
private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128);


     由以上代码我们可以知道:

  • corePoolSize为CPU数加一;

  • maximumPoolSize为CPU数的二倍加一;

  • 存活时间为1秒;

  • 任务缓存队列为LinkedBlockingQueue。

     

    现在,我们已经了解到了从我们调用AsyncTask对象的execute方法开始知道后台任务执行完都发生了什么。现在让我们回过头来看一看之前提到的postResult方法的源码:


private Result postResult(Result result) {
  @SuppressWarnings("unchecked")
  Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,      new AsyncTaskResult<Result>(this, result));
  message.sendToTarget();    
 return result; }

 

     在以上源码中,先调用了getHandler方法获取AsyncTask对象内部包含的sHandler,然后通过它发送了一个MESSAGE_POST_RESULT消息。我们来看看sHandler的相关代码:


private static final InternalHandler sHandler = new InternalHandler();
private static class InternalHandler extends Handler {
 public InternalHandler() {
   super(Looper.getMainLooper());
 }
 @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
 @Override
 public void handleMessage(Message msg) {
   AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
   switch (msg.what) {
     case MESSAGE_POST_RESULT:
       // There is only one result
result.mTask.finish(result.mData[0]);
       break;
     case MESSAGE_POST_PROGRESS:
       result.mTask.onProgressUpdate(result.mData);
      break;
   }
 }


     从以上代码中我们可以看到,sHandler是一个静态的Handler对象。我们知道创建Handler对象时需要当前线程的Looper,所以我们为了以后能够通过sHandler将执行环境从后台线程切换到主线程(即在主线程中执行handleMessage方法),我们必须使用主线程的Looper,因此必须在主线程中创建sHandler。这也就解释了为什么必须在主线程中加载AsyncTask类,是为了完成sHandler这个静态成员的初始化工作。

     在以上代码的handleMessage方法中,我们可以看到,当sHandler收到MESSAGE_POST_RESULT方法后,会调用finish方法,finish方法的源码如下:


private void finish(Result result) {
 if (isCancelled()) {
   onCancelled(result);
 } else {
   onPostExecute(result);
 }
 mStatus = Status.FINISHED;
}


    首先通过调用isCancelled方法判断AsyncTask任务是否被取消,若取消了则调用onCancelled方法,否则调用onPostExecute方法;接着把mStatus设为FINISHED,表示当前AsyncTask对象已经执行完毕。

    经过了以上的分析,我们大概了解了AsyncTask的内部运行逻辑,知道了它默认使用串行方式执行任务。那么如何让它以并行的方式执行任务呢? 阅读了以上的代码后,我们不难得到结论,只需调用executeOnExecutor方法,并传入THREAD_POOL_EXECUTOR作为其线程池即可。

 

三、延伸阅读

1. Java核心技术点之多线程: http://www.cnblogs.com/absfree/p/5327678.html

2. 深入理解Java之线程池: http://www.cnblogs.com/absfree/p/5357118.html

3. Callable&Future&FutureTask: http://www.cnblogs.com/dolphin0520/p/3949310.html


长按或扫描二维码关注我们,让您利用每天等地铁的时间就能学会怎样写出优质app。



 
Android开发探索 更多文章 阿里巴巴笔试题简析——Android中闪退的分析 [译]理解Android世界中的命名惯例 从源码角度看finish()方法的执行流程 十分钟理解Java中的弱引用 Android后台服务最佳实践
猜您喜欢 PHP新手必须掌握的入门与实战技巧 Android产品研发-->基类Activity HTML5将重塑Web世界? 【喜报】途牛又有多名员工获得发明专利奖励! “天猫双11狂欢夜”启发广告和卫视圈的是创新模式,革新技术圈的是实时互动