AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终的结果传递给主线程并在主线程中更新UI。
从实现上来说,AysncTask封装了Thread和Handler,通过AsyncTask可以更加方便地执行后台任务以及主线程中访问UI,但是AsyncTask并不适合进行特别耗时的后台操作,对于特别耗时的任务来说,建议使用线程池。
AsyncTask是一个抽象的泛型类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为AsyncTask类指定三个泛型参数,这三个参数的用途如下:
1.Params
在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
2.Progress
后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
3.Result
当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
因此,一个最简单的自定义AsyncTask就可以写成如下方式:
代码片1:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
……
}
这里我们把AsyncTask的第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。
当然,目前我们自定义的DownloadTask还是一个空任务,并不能进行任何实际的操作,我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下四个:
1.onPreExecute()
这个方法在主线程中执行,在异步任务执行之前,此方法会被调用。此方法一般用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
2.doInBackground(Params…params)
这个方法在线程池中执行,此方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果进行返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress…)方法来完成。
另外,此方法需要返回计算结果给onPostExecute。
3.onProgressUpdate(Progress…)
此方法在主线程中执行。当在后台任务中调用了publishProgress(Progress…)方法后,这个方法就很快会被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
4.onPostExecute(Result)
此方法在主线程中执行。当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。
上述这几个方法,onPreExecute先执行,接着是doInBackgroud,最后才是onPostExecute。除了上述四个方法以外,AsyncTask还提供了onCancelled()方法,它同样在主线程中执行,当异步任务被取消时,onCancelled()方法会被调用,这个使用onPostExecute则不会被调用。
一个比较完整的自定义AsyncTask就可以写成如下方式:
代码片2:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onPreExecute() {
progressDialog.show();
}
@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 onProgressUpdate(Integer... values) {
progressDialog.setMessage("当前下载进度:" + values[0] + "%");
}
@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();
}
}
}
这里我们模拟了一个下载任务,在doInBackground()方法中去执行具体的下载逻辑,在onProgressUpdate()方法中显示当前的下载进度,在onPostExecute()方法中来提示任务的执行结果。如果想要启动这个任务,只需要简单地调用以下代码即可:
代码片3:
//0到多个参数不等
new DownloadTask().execute();
以上就是AsyncTask的基本用法,怎么样,是不是感觉在子线程和UI线程之间进行切换变得灵活了很多?我们并不需求去考虑什么异步消息处理机制,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress()方法就可以轻松地从子线程切换到UI线程了。
说明(重点)
1.代码片3中execute() 是否传参数,以及传参数的值,影响代码片2第10行代码 doInBackground(Void… params)中params数组的值.
2.代码片2第11行代码 doInBackground(Void… params)的返回值,影响onPostExecute() 参数值及参数值类型.
3.而onProgressUpdate(Integer… values)的参数值及参数类型,是由doInBackground方法中调用AsyncTask的publishProgress方法确定参数值及参数类型;
上述的三点,是AsyncTask使用过程中最核心的部分(在第二部分实战案例中代码部分由注释).
为了便于更好的理解,这里通过一个真实的demo进行详细演示整个使用过程。
提供一个可以下载/继续、暂停下载、取消下载功能的demo.
界面如下:
这里重点介绍与 AsyncTask相关的代码,完整代码见源码:
先设置下载过程中的回调监听
代码片4
public interface DownloadListener {
void onProgress(int progress);
void onSuccess();
void onFailed();
void onPaused();
void onCanceled();
}
MainActivity.class中代码如下:
代码片5
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
String downFileUrl = "https://raw.githubusercontent" +
".com/guolindev/eclipse/master/eclipse-inst-win64" +
".exe";
private Context mContext;
private DownloadTask downloadTask;
private ProgressBar mProgressBar;
private TextView tv, status;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
mProgressBar = findViewById(R.id.progressbar);
tv = findViewById(R.id.tv);
status = findViewById(R.id.status);
findViewById(R.id.start_download).setOnClickListener(this);
findViewById(R.id.pause_download).setOnClickListener(this);
findViewById(R.id.cancel_download).setOnClickListener(this);
}
private DownloadListener listener = new DownloadListener() {
@Override
public void onProgress(int progress) {
mProgressBar.setProgress(progress);
tv.setText(progress + " %");
status.setText("当前状态:正在下载");
}
@Override
public void onSuccess() {
downloadTask = null;
mProgressBar.setVisibility(View.GONE);
Toast.makeText(mContext, "下载成功", Toast.LENGTH_SHORT).show();
status.setText("当前状态:下载完成");
}
@Override
public void onFailed() {
downloadTask = null;
mProgressBar.setVisibility(View.GONE);
Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show();
status.setText("当前状态:下载失败");
}
@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(mContext, "暂停", Toast.LENGTH_SHORT).show();
status.setText("当前状态:暂停下载");
}
@Override
public void onCanceled() {
downloadTask = null;
tv.setText(0 + " %");
status.setText("当前状态:取消下载");
mProgressBar.setVisibility(View.GONE);
Toast.makeText(mContext, "取消", Toast.LENGTH_SHORT).show();
String fileName = downFileUrl.substring(downFileUrl.lastIndexOf("/"));
String directory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + fileName);
if (file.exists()) {
file.delete();
}
}
};
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_download:
if (downloadTask == null) {
downloadTask = new DownloadTask(listener);
downloadTask.execute(downFileUrl);
}
break;
case R.id.pause_download:
if (downloadTask != null) {
downloadTask.pauseDownload();
}
break;
case R.id.cancel_download:
if (downloadTask != null) {
downloadTask.cancelDownload();
}
break;
default:
break;
}
}
}
MainActivity.class中代码都属于比较常规的代码,对控件进行实例化、三个按钮的点击事件,还有就是回调之后的操作。
第80-83行代码需要说明一下:
代码片6
if (downloadTask == null) {
downloadTask = new DownloadTask(listener);
downloadTask.execute(downFileUrl);
}
实例化AsyncTask, 调用execute()方法开始下载,并将下载文件的URL地址传入到execute()方法中。
重点看DownloadTask.class类:
代码片7
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
public static final int TYPE_SUCCESS = 0;
public static final int TYPE_FAILED = 1;
public static final int TYPE_PAUSED = 2;
public static final int TYPE_CANCELED = 3;
private DownloadListener listener;
private boolean isCanceled = false;
private boolean isPaused = false;
private int lastProgress;
public DownloadTask(DownloadListener listener) {
this.listener = listener;
}
//doInBackground方法返回值确定 第106行代码中 onPostExecute(Integer status) 方法的参数类型.
//doInBackground(String... params)方法的参数,由代码片5第82行代码downloadTask.execute(downFileUrl)决定.如果传入1个参数,String... params 就只有一个有值;如果传入2个参数,就params[0]、params[1]都对应有值.当前的demo只传了一个值downFileUrl,即第30行代码的params[0]其实就是为downFileUrl.
@Override
protected Integer doInBackground(String... params) {
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try {
long downloadedLength = 0; // 记录已下载的文件长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
file = new File(directory + fileName);
if (file.exists()) {
downloadedLength = file.length();
}
long contentLength = getContentLength(downloadUrl);
if (contentLength == 0) {
return TYPE_FAILED;
} else if (contentLength == downloadedLength) {
// 已下载字节和文件总字节相等,说明已经下载完成了
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// 断点下载,指定从哪个字节开始下载
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if (response != null) {
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadedLength); // 跳过已下载的字节
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = is.read(b)) != -1) {
if (isCanceled) {
return TYPE_CANCELED;
} else if (isPaused) {
return TYPE_PAUSED;
} else {
total += len;
savedFile.write(b, 0, len);
// 计算已下载的百分比
int progress = (int) ((total + downloadedLength) * 100 / contentLength);
//下面的方法决定第97行代码onProgressUpdate(Integer... values)中的参数类型、个数及对应的值
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCanceled && file != null) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if (progress > lastProgress) {
listener.onProgress(progress);
lastProgress = progress;
}
}
@Override
protected void onPostExecute(Integer status) {
switch (status) {
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_CANCELED:
listener.onCanceled();
default:
break;
}
}
public void pauseDownload() {
isPaused = true;
}
public void cancelDownload() {
isCanceled = true;
}
private long getContentLength(String downloadUrl) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if (response != null && response.isSuccessful()) {
long contentLength = response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
}
这段代码就比较长了,我们需要一步步地进行分析。
首先看一下AsyncTask中的3个泛型参数:第一个泛型参数指定为String,表示在执行AsyncTask的时候需要传入一个字符串参数给后台任务(下载文件的URL);第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位;第三个泛型参数指定为Integer,则表示使用整型数据来反馈执行结果(成功、失败、暂停、取消)。
接下来我们定义了4个整型常量用于表示下载的状态,TYPE_ SUCCESS 表示下载成功,TYPE_ FAILED 表示下载失败,TYPE_ PAUSED 表示暂停下载, TYPE_ CANCELED 表示取消下载。然后在DownloadTask的构造函数中要求传入一个刚刚定义的DownloadListener参数,我们待会就会将下载的状态通过这个参数进行回调。
接着就是要重写doInBackground()、onProgressUpdate( )和onPostExecute()这3个方法了,前面已经学习过这3个方法各自的作用,因此在这里它们各自所负责的任务也是明确的: doInBackground()方法用于在后台执行具体的下载逻辑,onProgressUpdate()方法用于在界面上更新当前的下载进度,onPostExecute()用于通知最终的下载结果。
那么先来看一下doInBackground()方法,首先我们从参数中获取到了下载的URL地址,并根据URL地址解析出了下载的文件名,然后指定将文件下载到Environment.DIRECTORY_DOWNLOADS目录下,也就是SD卡的Download目录。我们还要判断一下Download 目录中是不是已经存在要下载的文件了,如果已经存在的话则读取已下载的字节数,这样就可以在后面启用断点续传的功能。接下来先是调用了getContentLength()方法来获取待下载文件的总长度,如果文件长度等于0则说明文件有问题,直接返回TYPE_ FAILED,如果文件长度等于已下载文件长度,那么就说明文件已经下载完了,直接返回TYPE_ SUCCESS 即可。紧接着使用OKHttp来发送一条网络请求,需要注意的是,这里在请求中添加了一个header,用于告诉服务器我们想要从哪个字节开始下载,因为已下载过的部分就不需要再重新下载了。接下来读取服务器响应的数据,并使用Java 的文件流方式,不断从网络上读取数据,不断写入到本地,一直到文件全部下载完成为止。在这个过程中,我们还要判断用户有没有触发暂停或者取消的操作,如果有的话则返回TYPE_ PAUSED 或TYPE_ CANCELED 来中断下载,如果没有的话则实时计算当前的下载进度,然后调用publishProgress()方法进行通知。暂停和取消操作都是使用一个布尔型的变量来进行控制的,调用pauseDownload( )或cancelDownload()方法即可更改变量的值。
接下来看一下onProgressUpdate()方法,这个方法就简单得多了,它首先从参数中获取到当前的下载进度,然后和上一次的下载进度进行对比,如果有变化的话则调用DownloadListener的onProgress()方法来通知下载进度更新。
最后是onPostExecute()方法,也非常简单,就是根据参数中传入的下载状态来进行回调。下载成功就调用DownloadListener的onSuccess()方法,下载失败就调用onFailed()方法,暂停下载就调用onPaused()方法,取消下载就调用onCanceled()方法。
(1)AsyncTask的类必须在主线程中加载,这也意味着第一次访问AsyncTask必须发生在主线程,当然这个过程在Android4.1及以上版本中已经被系统自动完成。
(2)AsyncTask的对象必须在主线程中创建。
(3) execute方法必须在UI线程调用。
(4)不要在程序中直接调用onPreExecute() 、onPostExecute、doInBackgroud和onProgressUpdate方法。
(5)一个AsyncTask对象只能执行一次,即只能调用一次execute方法,否则会报运行时异常。
(6)在Android1.6之前,AsyncTask是串行执行任务的,Android 1.6的时候AsyncTask开始采用线程池里处理并行任务,但是从Android3.0开始,为了避免AsyncTask所带来的并发错误,AsyncTask又采用一个线程来串行执行任务。当然,在Android3.0以后,我们仍然可以通过AsyncTask的executeOnExecute方法来并行执行任务。
关于AsyncTask的使用到此结束,在下一篇,我们将对AsyncTask的源码进行解析。