Android异步任务AsyncTask的使用与原理分析

《Android缓存机制&一个缓存框架推荐》中说到,在了解了Android缓存机制后我准备自己动手写一个LruCache和DiskLruCache二级缓存的轻量级的图片请求框架,在思考如何搭建这个框架时,纠结于用何种方式去下载图片,是直接new出一个线程呢,还是用看起来稍微高大上档次一点的AsyncTask异步任务来处理?思来想去,还是虚荣心作怪,还是用AsyncTask吧,正好这个工具类我之前用的也比较少,对它的原理也不是很清楚,趁这个机会,好好学一下AsyncTask的使用,并分析一下其源码实现。待分析完AsyncTask之后,接着完成图片请求框架的编写。

AsyncTask的使用

分析AsyncTask原理之前,还是好好学习一下它的具体使用方法。

AsyncTask简介

在Android中,我们更新UI的操作必须要在主线程(UI线程)中进行,而下载图片、文件这种操作必须要在子线程中进行,Android为我们提供了Handler机制,实现了子线程与主线程之间的通信。通常做法就是先new出一个子线程Thread,在子线程中完成下载操作后,通过handler发送一条Message给主线程,主线程收到消息后,就可以进行UI的更新工作了,如下:

可以看到,每次要进行下载工作,我们就得先创建出Thread,然后在主线程中写好handler,为了对这个过程进行封装,Android提供了AsyncTask异步任务,AsyncTask对线程和handler进行了封装,使得我们可以直接在AsyncTask中进行UI的更新操作,就好像是在子线程进行UI更新一样。

创建AsyncTask子类

AsyncTask是一个抽象类,我们必须写一个子类继承它,在子类中完成具体的业务下载操作。为了对各种情况更好的封装,AsyncTask抽象类指定了三个泛型参数类型,如下:

其中,三个泛型类型参数的含义如下:

Params:开始异步任务执行时传入的参数类型,即doInBackground方法中的参数类型;

Progress:异步任务执行过程中,返回下载进度值的类型,即在doInBackground中调用publishProgress时传入的参数类型;

Result:异步任务执行完成后,返回的结果类型,即doInBackground方法的返回值类型;

有了这三个参数类型之后,也就控制了这个AsyncTask子类各个阶段的返回类型,如果有不同业务,我们就需要再另写一个AsyncTask的子类进行处理。

AsyncTask的回调方法

前面我们说过,AsyncTask对线程和handler进行了封装,那它的封装性体现在哪里呢?其实,AsyncTask的几个回调方法正是这种封装性的体现,使得我们感觉在子线程进行UI更新一样。一个基本的AsyncTask有如下几个回调方法:

(1)onPreExecute:在执行后台下载操作之前调用,运行在主线程中;

(2)doInBackground:核心方法,执行后台下载操作的方法,必须实现的一个方法,运行在子线程中;

(3)onPostExecute:后台下载操作完成后调用,运行在主线程中;

因此,AsyncTask的基本生命周期过程为:onPreExecute –> doInBackground –> onPostExecute。其中,onPreExecute 和onPostExecute分别在下载操作前和下载操作后调用,同时它们是在主线程中进行调用,因此可以在这两个方法中进行UI的更新操作,比如,在onPreExecute方法中,将下载等待动画显示出来,在onPostExecute方法中,将下载等待动画进行隐藏。

如果我们想向用户展示文件的下载进度情况,这时,我们可以在doInBackground下载操作中,调用publishProgress,将当前进度值传入该方法,而publishProgress内部会去调用AsyncTask的另一个回调方法:

(4)onProgressUpdate:在下载操作doInBackground中调用publishProgress时的回调方法,用于更新下载进度,运行在主线程中;

因此,在需要更新进度值时,AsyncTask的基本生命周期过程为:onPreExecute –> doInBackground –> publishProgress –> onProgressUpdate –> onPostExecute。

可以看到,AsyncTask的优秀之处在于几个回调方法的设置上,只有donInBackground是运行在子线程的,其他三个回调方法都是在主线程中运行,因此,只要在AsyncTask中,就可以实现文件的后台下载、UI的更新操作。

好了,明白了如何创建一个AsyncTask,以及AsyncTask的几个回调方法的调用时机,我们就可以来实战体验一下。在例子中,我们去下载一张图片,并通过进度条显示下载的进度。

首先实现一个AsyncTask的具体实现类,进行图片的下载,如下:

上面doInBackground中获取进度值时,我们只是为了做一个进度值更新调用的演示,实际项目文件下载中,我们可能会对拿到的输入流进行处理,比如读取输入流将文件保存到本地,在读取输入流的时候,我们就可以获取到已经读取的输入流大小作为进度值了,如下:

在MainActivity中使用:

布局文件如下:

效果如下:

由于需要联网,注意在AndroidManifest.xml中加入网络访问权限。

取消下载任务

我们先来看两个现象。 我们在布局中加一个按钮,点击这个按钮再加载一次图片,布局如下:

因此,在MainActivity中,我们就需要加入loadImage方法,如下:

现象一:在loadImage方法中,我们直接再次通过asyncTask.execute执行加载。看看此时效果如何:

onCreate中初始加载完一次图片后,我们点击“加载图片”按钮,此时程序直接崩溃了!这是因为,每一个new出的AsyncTask只能执行一次execute,如果同一个AsyncTask多次执行execute执行将会报错。

现象二:我们来修改loadImage方法,在该方法中,我们在打开自身MainActivity,使得多次初始化的时候进行加载,如下:

此时效果如下:

在第一次运行程序进入MainActivity,执行execute但在显示出图片之前,立即点击“加载图片”按钮,新打开一个MainActivity,我们发现这个MainActivity的进度条没有立即展示出进度出来,说明这个MainActivity的AsyncTask没有立即执行doInBackground,这是因为AsyncTask内部使用的是线程池,相当于里面有一个线程队列,执行一次execute时会将一个下载任务加入到线程队列,只有前一个任务完成了,下一个下载任务才会开始执行。

为了达到我们想要的效果,我们自然想到把上一个任务给取消掉。的确,AsyncTask为我们提供了cancel方法来取消一个任务的执行,但是要注意的是,cancel方法并没有能力真正去取消一个任务,其实只是设置这个任务的状态为取消状态,我们需要在doInBackground下载中进行检测,一旦检测到该任务被用户取消了,立即停止doInBackground方法的执行。

我们先修改MainActivity,根据不同业务需求,在不同地方进行任务的取消,我们这里在onPause中进行任务的取消,在MainActivity方法中加入onPause方法,如下:

继续修改AsyncTask,在这里面进行任务是否被取消的检测,这里我们只简单修改下doInBackground和onProgressUpdae方法,实际项目中开自己的业务逻辑来控制,如下:

代码交流 2021