Android AsyncTask 详解
Android AsyncTask 详解
内容划分
- AsyncTask简介
- 简单使用
- 繁杂部分和源码浅析
- 一些坑的地方
AsyncTask简介
AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.
AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.
这是Google Android 开发文档上关于AsyncTask的介绍,大概意思是AsyncTask设计为一个对于Thread和Handle的辅助类,主要让开发者方便的使用UI Thread和后台Thread的操作( 比如在后台线程下载文件,同时要在UI线程更新下载进度 )。同时这不是一个通用的多线程编程框架,他被设计为用于能够在最多几秒的时间内返回结果的任务。
简单使用
这里我们模拟一个后台下载一些文件,并在用户界面显示一个ProgressDialog来显示下载进度的功能。
/**
* author: zyinux
* date: on 2018/10/26
*/
public class DownloadTask extends AsyncTask<String,Integer,Boolean> {
ProgressDialog progressDialog;
Context context;
public DownloadTask(Context context) {
this.context=context;
}
@Override
protected void onPreExecute() {
progressDialog=new ProgressDialog(context);
progressDialog.setMax(100);
// progressDialog.setCancelable(false);
//注意这里我将上一行代码注释掉,使得dialog能够被取消,至于为什么这么做后面解释
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.show();
}
@Override
protected Boolean doInBackground(String... strings) {
int pro=0;
while (true){
pro++;
publishProgress(pro);
if (pro>=100){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
progressDialog.setProgress(values[0]);
}
@Override
protected void onPostExecute(Boolean aBoolean) {
progressDialog.dismiss();
if (aBoolean){
Toast.makeText(context,"下载成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(context,"下载失败",Toast.LENGTH_SHORT).show();
}
}
}
下面是Activity中调用的主要代码
new DownloadTask(this).execute("testurl");
//使用非常简单,new 之后执行execute传入执行的参数即可
运行效果如图
很普通的效果,下面来分析下上面的代码
首先是
public class DownloadTask extends AsyncTask<String,Integer,Boolean>
这一行泛型尖括号里的三个类型,具体对应三个
- Params 执行时发送给任务的参数的类型
- Progress 后台执行过程进度的类型
- Result 执行结果返回值的类型
当不需要这些参数的时候可以设置为<Void,Void,Void>
接着是实现的几个主要方法
protected void onPreExecute() {}
protected Boolean doInBackground(String... strings) {}
protected void onProgressUpdate(Integer... values) {}
protected void onPostExecute(Boolean aBoolean) { }
- onPreExecute 方法执行在UI线程,会在做后台任务之前调用,可以在这里执行一些初始化操作,例如上面的显示Dialog
- doInBackground 改方法执行在后台线程,任务中的耗时操作都应该在这里执行,AsyncTask内部维持了一个线程池,来对该方法调用执行优化。该方法的参数类型就是上面设置的 Params ,也就是执行调用代码中execute里传递来的参数。在该方法内部可以调用publishProgress方法来传递当前的进度。
- onProgressUpdate 在publishProgress方法后,系统会调用该方法,该方法运行在UI Thread,所以可以在这里做UI更新的操作,比如更新ProgressDialog的进度。这里传递的参数的类型就是上文里的 Progress。
- onPostExecute 在doInBackground方法执行完成后会执行该方法,同样运行在UI Thread。而传入的参数就是doInBackground方法的返回值,该类型由上文的Result指定。
繁杂部分和源码浅析
上面基本讲解了AsyncTask的使用方法了。细心的小伙伴可能注意到我上面的这两句代码
// progressDialog.setCancelable(false);
//注意这里我将上一行代码注释掉,使得dialog能够被取消,至于为什么这么做后面解释
现在来解释这里这么写的原因,假设我们运行app,并执行DownloadTask,这时候屏幕上弹出一个进度框,目前为止一切都没有问题。这个时候我们点击屏幕的其他地方,进度框会被取消掉,接着我们再次执行DownloadTask,小伙伴们猜猜现在会发生什么?
由于不太方便录屏和传gif图,我这里就简单说下会发生的事情:进度框会再度弹出,这没什么问题,但是进度条会停留在0%不动,直到一段时间之后弹出Toast显示下载完成,接着进度条开始慢慢增加,当达到百分之百时再次弹出Toast提示下载完成。
为什么会这样?
首先我们知道,取消dialog并不会取消掉AsyncTask,所以再次执行DownloadTask时,相当于此时有两个AsyncTask任务在执行。这就引出了一个问题,多个AsyncTask执行时是串行还是并行?
串行还是并行?
先说答案,默认是串行的,为什么,我们来看源码。
当执行 new DownloadTask(this).execute(“testurl”); 后:
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
接着继续看
@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
//看这里我们直到onPreExecute方法一定在doInBackground方法之前调用,并且是在UI Thread
onPreExecute();
/**
*具体执行方法在这里 我们直到这个exec就是上一步传进来的sDefaultExecutor
*这是一个用来管理线程池的框架
*/
exec.execute(mFuture);
return this;
}
初始化的地方,重点这里初始化为static final 是一个类静态变量,是类实例共享的
public static final Executor SERIAL_EXECUTOR = new 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);
}
}
}
将任务保存在ArrayDeque中,这是一个FIFO的队列,最后执行这个队列中的每一个任务。所以当执行多个AsyncTask时,他们是串行执行的。
上面说了这时一般情况,那么特殊情况呢?
DownloadTask task=new DownloadTask(this);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
这时候多个任务就不是一定排队按顺序执行了,具体执行顺序要看系统对线程的调度了,小伙伴们可以自己测试一下看看。不过一般不推荐这么使用,除非你有特殊需求。
一些坑的地方
关于cancel方法
public final boolean cancel(boolean mayInterruptIfRunning) {}
传入的参数表示当前任务执行时是否可以取消。但是当你的doInBackground方法中执行一个循环或者一个IO流读写任务,即使你传入了true,改方法也无法取消这个任务的执行。区别在于调用这个方法后,doInBackground执行完成时会调用onCancelled方法,而不是onPostExecute方法,所以cancel无法保证任务能够被取消
内存泄漏
上面的示列代码从Activity中传入了一个context。而AsyncTask的生命周期和Activity是无关的,那么当Activity被finish后,AsyncTask依然存在,而他持有着Activity的引用导致Activity无法被垃圾回收。
同时如果使用了非静态匿名内部类来实现AsyncTask,由于Java内部类的特点,他同样会持有当前Activity的引用造成内存泄漏。