1.前言
AsyncTask
类位于 android.os
包下。它使得开发者能够合适而容易地使用 UI 线程。具体来说,AsyncTask
允许执行后台任务并把执行结果发布到 UI 线程,无需开发者管理线程和 Handler
。AsyncTask 适合用于执行短时(最多几秒钟)的后台任务。如果执行长时间的后台任务,强烈建议使用 java.util.concurrent
包下提供的 API,如 Executor
,ThreadPoolExecutor
和 FutureTask
。
本篇基于 Android5.0, 来介绍 AsyncTask
的使用方式和版本演进。
2.使用方式
首先,看一下 AsyncTask
类的声明:
public abstract class AsyncTask<Params, Progress, Result>
这是一个抽象类,说明不允许直接使用 AsyncTask
;同时,它也是一个泛型类,拥有三个泛型,即 Params
,Progress
和 Result
。三个泛型的含义:
Params
,当任务执行时发送任务的参数类型;Progress
,在后台任务执行过程中发布的进度单元的类型;Result
,后台任务返回的结果的类型。
注意:
一个异步任务并不总是使用所有的泛型。使用Void
,来标明不使用某个泛型。
AsyncTask
提供了 4 个核心 API。当任务执行时,这些方法就会被调用:
onPreExecute()
:在任务执行前,在主线程调用。一般用来为任务执行做一些准备工作,比如在界面上显示一个等待加载的进度圈。doInBackground(Params... params)
:在onPreExecute()
完成后马上在后台线程调用。这个方法用来执行耗时的后台任务。这个方法会传入后台任务需要的参数。这个方法的返回值必须是后台任务的结果,并且返回值会作为onPostExecute(Result result)
的参数。在这个方法里,可以调用publishProgress(Progress... values)
来发布后台任务的执行进度到 UI 线程。这个方法是在实例化 AsyncTask 时必须覆写的方法。onProgressUpdate(Progress... values)
:在调用publishProgress(Progress... values)
后在主线程被调用。这个方法的执行时机是未定义的。用于后台任务进行过程中,在主线程展示进度信息。例如,展示一个进度条或者显示变动的进度数值。onPostExecute(Result result)
:在后台任务结束后,在主线程被调用。参数就是后台任务的执行结果,就是doInBackground(Params... params)
方法的返回值。
那么如何取消一个任务呢?
通过调用 AsyncTask
类的 cancel(boolean mayInterruptIfRunning)
方法,就可以在任何时候取消一个任务。调用这个方法之后,上面 4 个核心方法的执行序列会发生一些变化。具体而言,调用了 cancel
方法会引起后续调用的 isCancelled()
方法返回 true
。在 cancel
方法调用后,在 doInBackground(Params... params)
结束后,将会调用 onCancelled(Result result)
方法而非 onPostExecute(Result result)
方法。onCancelled(Result result) 方法也是在主线程被调用的。需要注意的是,为尽快确保一个任务是否已被取消,在 doInBackground(Params... params)
方法中需要判断 isCancelled()
方法的返回值。
下面通过一实例的例子,展示 AsyncTask
的使用方式。
相关代码如下:
public class AsyncTaskActivity extends Activity {
private static final String TAG = AsyncTaskActivity.class.getSimpleName();
private static final String url = "https://github.com/jhwsx/android-art-research/raw/"
+ "f6257e9f1e46848400f7ff2635991fd5a850d4f8/chapter_11/app-debug.apk";
private TextView mTvProgress;
private TextView mTvLength;
private DownloadTask mDownloadTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_asynctask);
Button btnStartDownload = (Button) findViewById(R.id.btn_start_download);
Button btnCancelDownload = (Button) findViewById(R.id.btn_cancel_download);
mTvProgress = (TextView) findViewById(R.id.tv_download_progress);
mTvLength = (TextView) findViewById(R.id.tv_download_length);
btnStartDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDownloadTask = new DownloadTask();
mDownloadTask.execute(url);
}
});
btnCancelDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDownloadTask.cancel(false);
}
});
}
public class DownloadTask extends AsyncTask<String, Integer, Long> {
private ProgressDialog mProgressDialog;
@Override
protected void onPreExecute() {
super.onPreExecute();
mProgressDialog = new ProgressDialog(AsyncTaskActivity.this);
mProgressDialog.setTitle("下载");
mProgressDialog.setMessage("获取数据中...");
mProgressDialog.show();
Log.d(TAG, "onPreExecute: threadName=" + Thread.currentThread().getName());
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
Log.d(TAG, "onProgressUpdate: threadName=" + Thread.currentThread().getName());
mTvProgress.setText("下载进度: " + values[0] + "%");
}
@Override
protected Long doInBackground(String... urls) {
Log.d(TAG, "doInBackground: threadName=" + Thread.currentThread().getName());
HttpURLConnection connection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
long contentLength = -1;
try {
URL url = new URL(urls[0]);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return -1L;
}
contentLength = connection.getContentLength();
inputStream = connection.getInputStream();
outputStream = new FileOutputStream(new File(getExternalFilesDir(null), "download.apk"));
byte[] buffer = new byte[4 * 1024];
int total = 0;
int length;
while ((length = inputStream.read(buffer)) != -1) {
total += length;
final int progress = (int) (total * 100f / contentLength);
if (isCancelled()) {
break;
}
publishProgress(progress);
outputStream.write(buffer, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
CloseUtils.closeIOQuietly(inputStream, outputStream);
if (connection != null) {
connection.disconnect();
}
}
return contentLength;
}
@Override
protected void onPostExecute(Long aLong) {
super.onPostExecute(aLong);
Log.d(TAG, "onPostExecute: threadName=" + Thread.currentThread().getName());
mTvLength.setText("下载字节数: " + aLong);
mTvLength.setVisibility(View.VISIBLE);
}
@Override
protected void onCancelled() {
super.onCancelled();
Log.d(TAG, "onCancelled: " + Thread.currentThread().getName());
mTvLength.setVisibility(View.VISIBLE);
mTvLength.setText("任务已取消");
}
}
}
运行日志:
当点击开始下载直至下载完成时,
D/AsyncTaskActivity: onPreExecute: threadName=main
D/AsyncTaskActivity: doInBackground: threadName=AsyncTask #1
D/AsyncTaskActivity: onProgressUpdate: progress=0, threadName=main
...
D/AsyncTaskActivity: onProgressUpdate: progress=99, threadName=main
D/AsyncTaskActivity: onProgressUpdate: progress=100, threadName=mai
D/AsyncTaskActivity: onPostExecute: threadName=main
从日志中可以看到,
- 4 个核心方法的调用次序:
onPreExecute
最先执行,接着是doInBackground
,然后是多个onProgressUpdate
,最后是onPostExecute
。 - 4 个核心方法的执行线程:只有
doInBackground
在后台线程中执行,其他都是在主线程执行。
当点击开始下载后,在获取下载进度后,再点击取消下载时:
D/AsyncTaskActivity: onPreExecute: threadName=main
D/AsyncTaskActivity: doInBackground: threadName=AsyncTask #2
D/AsyncTaskActivity: onProgressUpdate: progress=0, threadName=main
...
D/AsyncTaskActivity: onProgressUpdate: progress=86, threadName=main
D/AsyncTaskActivity: onCancelled: main
从日志中看以看到:
- 在取消任务时,就不会再调用
onPostExecute
方法,而是调用onCancelled
方法; onCancelled
方法是在主线程被调用的。
使用 AsyncTask
需要注意的地方:
AsyncTask
的类必须在主线程中加载。在 Android4.1 之前,这需要开发者自己完成;在 Android4.1 之后,这已经被系统自动完成,具体是在ActivityThread
类的main
方法中调用了AsyncTask
的init
方法;在 Android5.1 之后,是在AsyncTask
中使用Looper.getMainLooper()
类构造InternalHandler
静态成员变量,而不再使用AsyncTask
的init
方法了。AsyncTask
的对象必须在主线程中创建。AsyncTask
的execute
方法必须在 UI 线程调用。- 不要手动调用
onPreExecute
,onPostExecute
,doInBackground
和onProgressUpdate
方法。 - 一个
AsyncTask
对象只能执行一次(再次执行,会抛出异常),这一点可以在executeOnExecutor
方法中看出来。 - 要记得在合适的地方清理或者说撤销
AsyncTask
,否则会引起内存泄露,或者引起 UI 更新问题。可以选择在onStop
或者onDestroy
方法中调用AsyncTask
的cancel
方法。cancel
方法有两种工作模式:温和的和粗暴的,需要选择使用。它们的区别是:cancel(false)
方法,会温和地设置isCancelled()
的状态为true
。随后,AsyncTask
会检查doInBackground
方法中的isCanclled()
方法的状态,然后选择提前结束运行。而cancel(true)
方法,则会简单粗暴地结束doInBackground
方法所在的线程。
3.版本演进
当 Android 初次引入 AsyncTask
时,AsyncTask
是以串行方式执行在单线程上的。从 Android1.6 开始,AsyncTask
转而使用线程池,允许多个任务并行执行。从 Android3.0 开始,AsyncTask
是执行在单线程上,这是为了避免并发执行引起的程序错误。不过,如果开发者确实需要并行执行任务,那么可以调用 executeOnExecutor
方法,使用 THREAD_POOL_EXECUTOR
作为参数。
4.总结
本文先介绍了 AsyncTask
的 泛型,核心 API;接着引入了一个实际的例子,验证了一些描述;最后简单介绍了 AsyncTask 的版本演进。希望能加深对于 AsyncTask
类的认识与理解。