AsyncTask原理

AsyncTask原理为什么要用 AsyncTask 我们知道 Android 应用的主线程 UI 线程 是线程不安全的 负责前台用户界面的绘制以及响应用户的操作 肩负着绘制用户界面和及时响应用户操作的重任 为了避免 用户点击按钮后没反应 这样的糟糕用户体验 我们就要确保主线程时刻保持着较高的响应性 主线程不能够运行需要占用大量 CPU 时间片的任务 如大量复杂的浮点运算 较大的磁盘 IO 操作 网络 socket 等 为了做到这一点

为什么要用AsyncTask

我们知道,Android应用的主线程(UI 线程,是线程不安全的,负责前台用户界面的绘制以及响应用户的操作)肩负着绘制用户界面和及时响应用户操作的重任,为了避免“用户点击按钮后没反应”这样的糟糕用户体验,我们就要确保主线程时刻保持着较高的响应性,主线程不能够运行需要占用大量CPU时间片的任务(如大量复杂的浮点运算,较大的磁盘IO操作,网络socket等)。为了做到这一点,我们就要把耗时的任务移出主线程,那么耗时的任务交给谁来完成呢?答案就是工作者线程。如果想要在子线程里进行UI操作,让工作者线程在后台执行一些比较耗时的任务,就需要借助Android的异步消息处理机制。不过为了更加方便我们在子线程中更新UI元素,Android中的工作者线程主要有AsyncTask、IntentService、HandlerThread,它们本质上都是对线程或线程池的封装。Android从1.5版本就引入了一个AsyncTask类,使用它就可以非常灵活方便地从子线程切换到UI线程,我们本篇文章的主角也就正是它了。

总的来说,我们使用工作者线程是因为主线程已经有很多活要干了,累活就得交给别人干。AsyncTask是我们日常中广泛使用的一种工作者线程,它的方便之处在于可以在后台任务执行完毕时根据返回结果相应的更新UI。AsyncTask很早就出现在Android的API里了,所以我相信大多数朋友对它的用法都已经非常熟悉。不过今天我还是准备从AsyncTask的基本用法开始讲起,然后我们再来一起分析下AsyncTask源码,看看它是如何实现的,最后我会介绍一些关于AsyncTask你所不知道的秘密。

怎么用AsyncTask

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

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

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

public abstract class AsyncTask 
   
  

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

  • Params:在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
  • Progress:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  • Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

一个最简单的自定义AsyncTask就可以写成如下方式:

class DownloadTask extends AsyncTask<Void, Integer, Boolean> { 
    …… } 

这里我们把AsyncTask的第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。

当然,目前我们自定义的DownloadTask还是一个空任务,并不能进行任何实际的操作,我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。

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

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

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

我们还是简单介绍下AsyncTask一些使用示例。我们先新建一个类DemoAsyncTask继承AsyncTask,因为AsyncTask是抽象类,其中doInBackground方法必须重写。

private class DownloadTask extends AsyncTask 
  
    { @Override 
   protected 
   void 
   onPreExecute() { super.onPreExecute(); } @Override 
   protected Boolean 
   doInBackground(Void... 
   params) { 
   try { 
   while ( 
   true) { 
   int downloadPercent = doDownload(); publishProgress(downloadPercent); 
   if (downloadPercent >= 
   100) { 
   break; } } } 
   catch (Exception e) { 
   return 
   false; } 
   return 
   true; } @Override 
   protected 
   void 
   onPostExecute(Boolean result) { progressDialog.dismiss(); 
   if (result) { Toast.makeText(context, 
   "下载成功", Toast.LENGTH_SHORT).show(); } 
   else { Toast.makeText(context, 
   "下载失败", Toast.LENGTH_SHORT).show(); } } @Override 
   protected 
   void 
   onProgressUpdate(Integer... values) { progressDialog.setMessage( 
   "当前下载进度:" + values[ 
   0] + 
   "%"); } @Override 
   protected 
   void 
   onCancelled(Void aVoid) { super.onCancelled(aVoid); } @Override 
   protected 
   void 
   onCancelled() { super.onCancelled(); } } DemoAsyncTask task = 
   new DemoAsyncTask(); task.execute( 
   "demo test AsyncTask"); 
   //task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, "test"); 
   //myTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "test"); 
  

这里我们模拟了一个下载任务,在doInBackground()方法中去执行具体的下载逻辑,在onProgressUpdate()方法中显示当前的下载进度,在onPostExecute()方法中来提示任务的执行结果。如果想要启动这个任务,只需要简单地调用以下代码即可:

new DownloadTask().execute(); 

AsyncTask的局限性

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

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

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

探索AsyncTask的工作原理

虽然AsyncTask这么简单好用,但你知道它是怎样实现的吗?那么接下来,我们就来分析一下AsyncTask的源码,对它的实现原理一探究竟。注意这里我选用的是Android 4.0的源码,如果你查看的是其它版本的源码,可能会有一些出入。

从之前DownloadTask的代码就可以看出,在启动某一个任务之前,要先new出它的实例,因此,我们就先来看一看AsyncTask构造函数中的源码,如下所示:

public AsyncTask() { mWorker = new WorkerRunnable 
  
    () { 
   public Result 
   call() 
   throws Exception { mTaskInvoked.set( 
   true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 
   return postResult(doInBackground(mParams)); } }; mFuture = 
   new FutureTask 
   
     (mWorker) { 
    @Override 
    protected 
    void 
    done() { 
    try { 
    final Result result = get(); postResultIfNotInvoked(result); } 
    catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } 
    catch (ExecutionException e) { 
    throw 
    new RuntimeException( 
    "An error occured while executing doInBackground()", e.getCause()); } 
    catch (CancellationException e) { postResultIfNotInvoked( 
    null); } 
    catch (Throwable t) { 
    throw 
    new RuntimeException( 
    "An error occured while executing " + 
    "doInBackground()", t); } } }; } 
    
  

这段代码虽然看起来有点长,但实际上并没有任何具体的逻辑会得到执行,只是初始化了两个变量,并在初始化mFuture的时候将mWorker作为参数传入。在第2行到第8行,初始化了mWorker,它是一个派生自WorkRunnable类的对象。WorkRunnable是一个抽象类,它实现了Callable< Result>接口,Callback接口是JDK1.5加入的高级并发架包里面的一个接口,它可以有一个泛型返回值。我们再来看一下第3行开始的call方法的定义,首先将mTaskInvoked设为true表示当前任务已被调用过,然后在第5行设置线程的优先级。在第6行我们可以看到,调用了AsyncTask对象的doInBackground方法开始执行我们所定义的后台任务,并获取返回结果存入result中。最后将任务返回结果传递给postResult方法。关于postResult方法我们会在下文进行分析。由此我们可以知道,实际上AsyncTask的成员mWorker包含了AyncTask最终要执行的任务(即mWorker的call方法)。

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

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

也就是说,mFuture是一个封装了我们的后台任务的FutureTask对象,FutureTask类实现了FutureRunnable接口,通过这个接口可以方便的取消后台任务以及获取后台任务的执行结果,具体介绍请看:Java并发编程:Callable、Future和FutureTask。

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

接着如果想要启动某一个任务,就需要调用该任务的execute()方法,因此现在我们来看一看execute()方法的源码,如下所示:

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

简单的有点过分了,只有一行代码,仅是调用了executeOnExecutor()方法,我们可以看到它接收的参数是Params类型的参数,这个参数会一路传递到doInBackground方法中。execute方法仅仅是调用了executeOnExecutor方法,并将executeOnExecutor方法的返回值作为自己的返回值。我们注意到,传入了sDefaultExecutor作为executeOnExecutor方法的参数,那么sDefaultExecutor是什么呢?简单的说,它是AsyncTask的默认执行器(线程池)。AsyncTask可以以串行(一个接一个的执行)或并行(一并执行)两种方式来执行后台任务,在Android3.0及以后的版本中,默认的执行方式是串行。这个sDefaultExecutor就代表了默认的串行执行器(线程池)。也就是说我们平常在AsyncTask对象上调用execute方法,使用的是串行方式来执行后台任务。那么具体的逻辑就应该写在这个方法里了,快跟进去瞧一瞧:

public final AsyncTask 
   
   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; } 
  

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

在第15行调用了onPreExecute()方法,因此证明了onPreExecute()方法会第一个得到执行。可是接下来的代码就看不明白了,怎么没见到哪里有调用doInBackground()方法呢?

接着我们看到,在第17行调用了Executor的execute()方法,并将前面初始化的mFuture对象传了进去,那么这个Executor对象又是什么呢?查看上面的execute()方法,原来是传入了一个sDefaultExecutor变量,接着找一下这个sDefaultExecutor变量是在哪里定义的,源码如下所示:

public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); …… private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; 

可以看到,这里先new出了一个SERIAL_EXECUTOR常量,然后将sDefaultExecutor的值赋值为这个常量,也就是说明,刚才在executeOnExecutor()方法中调用的execute()方法,其实也就是调用的SerialExecutor类中的execute()方法。那么我们自然要去看看SerialExecutor的源码了,如下所示:

private static class SerialExecutor implements Executor { 
    final ArrayDeque 
  
    mTasks = 
   new ArrayDeque 
   
     (); 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这个串行线程池的任务缓存队列,在第6行,我们用offer方法向任务缓存队列中添加一个任务,任务的内容如第7行到第13行的run方法定义所示。我们可以看到,run方法中:第9行调用了mFuture(第5行的参数r就是我们传入的mFuture)的run方法,而mFuture的run方法内部会调用mWorker的call方法,然后就会调用doInBackground方法,我们的后台任务也就开始执行了。那么我们提交到任务缓存队列中的任务什么时候会被执行呢?我们接着往下看。

首先我们看到第三行定义了一个Runnable变量mActive,它代表了当前正在执行的AsyncTask对象。第15行判断mActive是否为null,若为null,就调用scheduleNext方法。如第20行到24行所示,在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 
  
    sPoolWorkQueue = 
   new LinkedBlockingQueue 
   
     ( 
    128); 
    
  

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

  • corePoolSize为CPU数加一;
  • maximumPoolSize为CPU数的二倍加一;
  • 存活时间为1秒;
  • 任务缓存队列为LinkedBlockingQueue。

SerialExecutor类中也有一个execute()方法,这个方法里的所有逻辑就是在子线程中执行的了,注意这个方法有一个Runnable参数,那么目前这个参数的值是什么呢?当然就是mFuture对象了,也就是说在第9行我们要调用的是FutureTask类的run()方法,而在这个方法里又会去调用Sync内部类的innerRun()方法,因此我们直接来看innerRun()方法的源码:

void innerRun() { if (!compareAndSetState(READY, RUNNING)) return; runner = Thread.currentThread(); if (getState() == RUNNING) { 
   // recheck after setting thread  V result; try { result = callable.call(); } catch (Throwable ex) { setException(ex); return; } set(result); } else { releaseShared(0); // cancel  } } 

可以看到,在第8行调用了callable的call()方法,那么这个callable对象是什么呢?其实就是在初始化mFuture对象时传入的mWorker对象了,此时调用的call()方法,也就是一开始在AsyncTask的构造函数中指定的,我们把它单独拿出来看一下,代码如下所示:

public Result call() throws Exception { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); return postResult(doInBackground(mParams)); } 

在postResult()方法的参数里面,我们终于找到了doInBackground()方法的调用处,虽然经过了很多周转,但目前的代码仍然是运行在子线程当中的,所以这也就是为什么我们可以在doInBackground()方法中去处理耗时的逻辑。接着将doInBackground()方法返回的结果传递给了postResult()方法,这个方法的源码如下所示:

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

如果你已经熟悉了异步消息处理机制,这段代码对你来说一定非常简单吧。这里使用sHandler对象发出了一条消息,消息中携带了MESSAGE_POST_RESULT常量和一个表示任务执行结果的AsyncTaskResult对象。这个sHandler对象是InternalHandler类的一个实例,那么稍后这条消息肯定会在InternalHandler的handleMessage()方法中被处理。InternalHandler的源码如下所示:

private static class InternalHandler extends Handler { 
    @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这个静态成员的初始化工作。

这里对消息的类型进行了判断,如果这是一条MESSAGE_POST_RESULT消息,就会去执行finish()方法,如果这是一条MESSAGE_POST_PROGRESS消息,就会去执行onProgressUpdate()方法。那么finish()方法的源码如下所示:

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

在第2行,会通过调用isCancelled方法判断AsyncTask任务是否被取消,若取消了则调用onCancelled()方法,否则调用onPostExecute()方法;在第7行,把mStatus设为FINISHED,表示当前AsyncTask对象已经执行完毕,这样当前任务的执行就全部结束了。

我们注意到,在刚才InternalHandler的handleMessage()方法里,还有一种MESSAGE_POST_PROGRESS的消息类型,这种消息是用于当前进度的,调用的正是onProgressUpdate()方法,那么什么时候才会发出这样一条消息呢?相信你已经猜到了,查看publishProgress()方法的源码,如下所示:

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

读到这里,非常清晰了吧!现在其实我们已经把AsyncTask整个执行任务的过程走完了,其中暴露给我们的那几个回调方法也都走到了。正因如此,在doInBackground()方法中调用publishProgress()方法才可以从子线程切换到UI线程,从而完成对UI元素的更新操作。其实也没有什么神秘的,因为说到底,AsyncTask也是使用的异步消息处理机制,只是做了非常好的封装而已。

现在我们回过头来看,AsyncTask其实只是对JDK 1.5提供的高级并发特性,concurrent架包做的一个封装,方便开发者来处理异步任务,当然里面还有很多细节处理的方法值得大家学习,如任务执行进度的反馈,任务执行原子性的保证等,这些留给大家自己学习了。

相信你对AsyncTask中的每个回调方法的作用、原理、以及何时会被调用都已经搞明白了吧。

关于AsyncTask你所不知道的秘密

不得不说,刚才我们在分析SerialExecutor的时候,其实并没有分析的很仔细,仅仅只是关注了它会调用mFuture中的run()方法,但是至于什么时候会调用我们并没有进一步地研究。其实SerialExecutor也是AsyncTask在3.0版本以后做了最主要的修改的地方,它在AsyncTask中是以常量的形式被使用的,因此在整个应用程序中的所有AsyncTask实例都会共用同一个SerialExecutor。下面我们就来对这个类进行更加详细的分析,为了方便阅读,我把它的代码再贴出来一遍:

private static class SerialExecutor implements Executor { 
    final ArrayDeque 
  
    mTasks = 
   new ArrayDeque 
   
     (); 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是使用ArrayDeque这个队列来管理Runnable对象的,如果我们一次性启动了很多个任务,首先在第一次运行execute()方法的时候,会调用ArrayDeque的offer()方法将传入的Runnable对象添加到队列的尾部,然后判断mActive对象是不是等于null,第一次运行当然是等于null了,于是会调用scheduleNext()方法。在这个方法中会从队列的头部取值,并赋值给mActive对象,然后调用THREAD_POOL_EXECUTOR去执行取出的取出的Runnable对象。之后如何又有新的任务被执行,同样还会调用offer()方法将传入的Runnable添加到队列的尾部,但是再去给mActive对象做非空检查的时候就会发现mActive对象已经不再是null了,于是就不会再调用scheduleNext()方法。

那么后面添加的任务岂不是永远得不到处理了?当然不是,看一看offer()方法里传入的Runnable匿名类,这里使用了一个try finally代码块,并在finally中调用了scheduleNext()方法,保证无论发生什么情况,这个方法都会被调用。也就是说,每次当一个任务执行完毕后,下一个任务才会得到执行,SerialExecutor模仿的是单一线程池的效果,如果我们快速地启动了很多任务,同一时刻只会有一个线程正在执行,其余的均处于等待状态。Android照片墙应用实现,再多的图片也不怕崩溃 这篇文章中例子的运行结果也证实了这个结论。

不过你可能还不知道,在Android 3.0之前是并没有SerialExecutor这个类的,那个时候是直接在AsyncTask中构建了一个sExecutor常量,并对线程池总大小,同一时刻能够运行的线程数做了规定,代码如下所示:

private static final int CORE_POOL_SIZE = 5; private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 10; …… private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory); 

可以看到,这里规定同一时刻能够运行的线程数为5个,线程池总大小为128。也就是说当我们启动了10个任务时,只有5个任务能够立刻执行,另外的5个任务则需要等待,当有一个任务执行完毕后,第6个任务才会启动,以此类推。而线程池中最大能存放的线程数是128个,当我们尝试去添加第129个任务时,程序就会崩溃。

因此在3.0版本中AsyncTask的改动还是挺大的,在3.0之前的AsyncTask可以同时有5个任务在执行,而3.0之后的AsyncTask同时只能有1个任务在执行。为什么升级之后可以同时执行的任务数反而变少了呢?这是因为更新后的AsyncTask已变得更加灵活,如果不想使用默认的线程池,还可以自由地进行配置。比如使用如下的代码来启动任务:

Executor exec = new ThreadPoolExecutor(15, 200, 10, TimeUnit.SECONDS, new LinkedBlockingQueue 
  
    ()); 
   new DownloadTask().executeOnExecutor(exec); 
  

这样就可以使用我们自定义的一个Executor来执行任务,而不是使用SerialExecutor。上述代码的效果允许在同一时刻有15个任务正在执行,并且最多能够存储200个任务。

使用AsyncTask一点小技巧

我们以一个实例来说明,“点击按钮开始下载Android安装包,然后显示一个对话框来反馈下载进度”。我们先初始化一个对话框,由于要显示进度,我们用Github上面一个能够显示百分比的进度条 NumberProgressbar,启动任务的按钮我们使用 circlebutton,一个有酷炫动画的按钮,Github上面有很多非常好的开源项目,当然炫酷的控件是其中一部分了,后面有机会,会去学习一些比较流行的控件它们的实现原理,今天就暂且拿来主义了~~。

1.先初始化进度条提示对话框。

builder = new AlertDialog.Builder(MainActivity.this); LayoutInflater inflater = LayoutInflater.from(MainActivity.this); mDialogView = inflater.inflate(R.layout.progress_dialog_layout, null); mNumberProgressBar = (NumberProgressBar)mDialogView.findViewById(R.id.number_progress_bar); builder.setView(mDialogView); mDialog = builder.create();

2.设置按钮点击事件。

findViewById(R.id.circle_btn).setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { dismissDialog(); mNumberProgressBar.setProgress(0); myTask = new MyAsyncTask(); myTask.execute(DownloadUrl); } });

3.DownloadAsyncTask实现,有点长。

private class DownloadAsyncTask extends AsyncTask 
  
    { @Override 
   protected 
   void 
   onPreExecute() { super.onPreExecute(); mDialog.show(); } @Override 
   protected 
   void 
   onPostExecute(String aVoid) { super.onPostExecute(aVoid); dismissDialog(); } @Override 
   protected 
   void 
   onProgressUpdate(Integer... values) { super.onProgressUpdate(values); mNumberProgressBar.setProgress(values[ 
   0]); } @Override 
   protected 
   void 
   onCancelled(String aVoid) { super.onCancelled(aVoid); dismissDialog(); } @Override 
   protected 
   void 
   onCancelled() { super.onCancelled(); dismissDialog(); } @Override 
   protected String 
   doInBackground(String... 
   params) { String urlStr = 
   params[ 
   0]; FileOutputStream output = 
   null; 
   try { URL url = 
   new URL(urlStr); HttpURLConnection connection = (HttpURLConnection)url.openConnection(); String ApkFile = 
   "ApkFile"; File file = 
   new File(Environment.getExternalStorageDirectory() + 
   "/" + ApkFile); 
   if (file.exists()) { file.delete(); } file.createNewFile(); InputStream input = connection.getInputStream(); output = 
   new FileOutputStream(file); 
   int total = connection.getContentLength(); 
   if (total <= 
   0) { 
   return 
   null; } 
   int plus = 
   0; 
   int totalRead = 
   0; 
   byte[] buffer = 
   new 
   byte[ 
   4* 
   1024]; 
   while((plus = input.read(buffer)) != - 
   1){ output.write(buffer); totalRead += plus; publishProgress(totalRead * 
   100 / total); 
   if (isCancelled()) { 
   break; } } output.flush(); } 
   catch (MalformedURLException e) { e.printStackTrace(); 
   if (output != 
   null) { 
   try { output.close(); } 
   catch (IOException e2) { e2.printStackTrace(); } } } 
   catch (IOException e) { e.printStackTrace(); 
   if (output != 
   null) { 
   try { output.close(); } 
   catch (IOException e2) { e2.printStackTrace(); } } } 
   finally { 
   if (output != 
   null) { 
   try { output.close(); } 
   catch (IOException e) { e.printStackTrace(); } } } 
   return 
   null; } } 
  

这样一个简单的下载文件文件就基本实现了,到目前为止谈不上技巧,但是现在我们有一个问题,就是如果我们的Activity正在后台执行一个任务,可能耗时较长,那用户可能会点击返回退出Activity或者退出App,那么后台任务不会立即退出,如果AsyncTask内部有Activity中成员变量的引用,还会造成Activity的回收延时,造成一段时间内的内存泄露,所以我们需要加上下面的第四步处理。

4.onPause中判断应用是否要退出,从而决定是否取消AsyncTask执行。

@Override protected void onPause() { super.onPause(); if (myTask != null && isFinishing()) { myTask.cancel(false); } }

这样我们的异步任务就会在Activity退出时,也随之取消任务执行,顺利被系统销毁回收,第四步很多时候会被遗漏,而且一般也不会有什么致命的问题,但是一旦出问题了,就很难排查,所以遵循编码规范还是有必要的。

希望大家读完能有所收获O(∩_∩)O~

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/214543.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午4:00
下一篇 2026年3月18日 下午4:01


相关推荐

  • ora01017 linux,ORA-01017: invalid username/password; logon denied 解决办法

    ora01017 linux,ORA-01017: invalid username/password; logon denied 解决办法ORA-01017:invalidusername/password;logondenied解决办法环境介绍:操作系统RHEL6.4X64、数据库:ORACLE11.2.0.4.0;在现有环境下手动创建数据库,在数据库的手动创建过程中没有大的问题,但在数据库创建完成后出现在本地可以登录数据库用户包括管理员账户,通过客户端远端无法登录报如下错误:通过测试,或者在数据库系统中更换用户账户口…

    2022年5月6日
    276
  • 如何使用 PyCharm 将代码上传到GitHub上(详细图解)

    如何使用 PyCharm 将代码上传到GitHub上(详细图解)说明 该篇博客是博主一字一码编写的 实属不易 请尊重原创 谢谢大家 一丶说明测试条件 需要有 GitHub 账号以及在本地安装了 Git 工具 无论是 Linux 环境还是 Windows 都是一样的如果还没有 GitHub 账号的同学请查看该篇博客 https blog csdn net article details WindowsGit 安装 ht

    2026年3月27日
    2
  • Nginx + Tomcat 搭建负载均衡

    Nginx + Tomcat 搭建负载均衡Nginx + Tomcat 搭建负载均衡

    2022年4月22日
    36
  • 106-Latex矩阵过大问题

    106-Latex矩阵过大问题参考 LaTeX 技巧 376 如何解除 amsmath 中的 bmatrix 限制 10 列 在 Latex 中编辑矩阵 有时提示行数或列数过多 比如下面这个 begin bmatrix 0 amp 0 amp 0 amp 0 amp 0 amp 0 amp 0 amp 0 amp 0 amp 0 amp 0 amp 0 0 amp 0 amp 0 amp a

    2026年3月18日
    2
  • 生活学习心态体会

    生活学习心态体会1 善于学习的人 2 有开放心态的人 3 大家都愿意与之交往的人 4 敢于挑战自身弱点的人 5 愿意接触新鲜事物的人 6 乐于分享的人 7 常怀感恩之心的人 8 有自知之明的人 9 脚踏实地的人 10 有眼光并能抓住机会的人

    2026年3月16日
    3
  • Mysql创建新用户方法

    1.CREATE USER语法:CREATE USER 'username'@'host' IDENTIFIED&#160

    2021年12月27日
    41

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号