大话Android多线程(六) AsyncTask知识扫盲

2018-02-24 13:13:17来源:https://juejin.im/post/5a85a6066fb9a06337573955作者:稀土掘金人点击

分享

本章我们将结合之前几篇博客,来研究研究多线程知识综合应用程度很高的 AsyncTask 类(Android 7.0版本)


往期回顾 大话Android多线程(一) Thread和Runnable的联系和区别 大话Android多线程(二) synchronized使用解析 大话Android多线程(三) 线程间的通信机制之Handler 大话Android多线程(四) Callable、Future和FutureTask 大话Android多线程(五) 线程池ThreadPoolExecutor详解


AsyncTask简介

通过之前几篇博客的学习和研究,我们知道了要将 耗时的任务 放到 子线程 中执行,然后使用 Handler 机制通知 UI线程 任务的结果并执行 更新UI 的操作。如果这些步骤都由我们自己动手去写,势必会让代码显得非常臃肿


Android给我们提供了一种轻量级的异步任务类 AsyncTask ,该类实现了异步操作,并提供相应的接口反馈 异步任务执行结果及进度 ,实现了 从子线程执行任务到通知主线程更新UI 的一条龙服务,大大减少了我们的开发工作。下面我们将从如何使用开始逐步揭开 AsyncTask 的神秘面纱


如何使用AsyncTask

我们以去快餐店点餐为例。我们将顾客点餐与取餐的行为放在 主线程 中( 更新UI界面 等操作),而服务人员在厨房配餐的行为放在 子线程 中进行(在后台执行 耗时操作 )


顾客不会关心服务人员在厨房是如何工作的,他们只关心点了什么(配置参数并调用 AsyncTask.execute 进行提交)以及何时收到通知去取餐(在 AsyncTask.onPostExecute 回调方法中接收 后台任务返回的结果 ,并执行相应的操作)。服务人员在配餐之前可以帮助顾客准备餐盘、纸巾等等,当然这些工作对顾客来说是可见的(通过重写 AsyncTask.onPreExecute 回调方法执行一些耗时任务之前的准备工作,该方法运行在 主线程 中)。准备工作完毕后,服务人员会通知厨房进行配餐,这部分工作对于顾客来说是不可见的(在 AsyncTask.doInBackground 方法中编写执行 耗时任务 的代码,这些 耗时任务 运行在 子线程 中)。配餐完毕后,通知顾客来取餐( AsyncTask.doInBackground 结束时会返回一个值,该值会传递到 AsyncTask.onPostExecute 中,证明耗时任务已经执行完毕)


整个流程简单总结一下就是 开始任务 ( execute ) → 任务准备 ( onPreExecute ) → 执行任务 ( doInBackground ) → 反馈任务结果 → 回到主线程执行相应操作 ( onPostExecute )


下面我们来看具体的代码(关于使用 AsyncTask 会导致 内存泄漏 的问题请看文末的补充,这里的代码只是简单实现就不多赘述了)


public class AsyncTaskTestActivity extends AppCompatActivity {
TextView textShow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task_test);
textShow = (TextView) findViewById(R.id.text_show);
}
public void clickEvent(View view) {
switch (view.getId()) {
case R.id.btn_start:
List<String> list = new ArrayList<>();
list.add("薯条");
list.add("汉堡");
list.add("可乐");
new MyAsyncTask().execute(list);
break;
}
}
public class MyAsyncTask extends AsyncTask<List<String>,String,String>{
@Override
protected void onPreExecute() {
super.onPreExecute();
textShow.setText("餐盘准备好了,开始配餐...");
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
textShow.setText("配餐完毕," + s);
}
@Override
protected String doInBackground(List<String>... params) {
String foods = "已经配好的食物 —— ";
try {
for (String str : params[0]){
Thread.sleep(1000);//模拟配餐的时间
foods = foods + str + " ";
Log.e("白胡子快餐店",foods);
}
}catch (Exception e){}
return foods;
}
}
}

运行效果如图所示








我们来分析一下上述代码中的细节


使用 AsyncTask 首先得实现它的子类,我们先来看下抽象类 AsyncTask 的部分源码


public abstract class AsyncTask<Params, Progress, Result>

这里告诉我们如果要继承 AsyncTask ,需配置3个 泛型参数 的具体类型,这3个参数的介绍如下


Params : 开始执行异步任务 时需传入的参数类型,即 AsyncTask.execute 方法中要传递的参数类型。例如我们将 Params 设为 String 类型,那么将调用 execute(String s) 方法 开始任务 ,提交的参数 s 类型为 String 。另外此参数类型同样和传入 AsyncTask.doInBackground 方法的参数相对应

ps:如果不需要传递任何参数,则可以将参数类型设为 Void ,那么 开始任务 时只需要调用 execute() 即可,下同(返回参数同理)


Progress : 执行异步任务过程中 向 主线程 传递的 进度值的类型 。我们可以在 AsyncTask.doInBackground 方法中调用 publishProgress 方法告知 主线程 当前耗时任务的执行进度,我们设置的 进度值类型 即 publishProgress 方法要传递参数的类型
Result : 任务执行完毕 后,返回的 结果类型 ,即 AsyncTask.doInBackground 方法 返回值 的类型,也是 AsyncTask.onPostExecute 方法 传入参数 的类型

结合之前的代码,在我们实现的 AsyncTask 子类中, Params 设为 List<String> 类型, Progress 设为 String 类型, Result 设为 String 类型


public class MyAsyncTask extends AsyncTask<List<String>,String,String>

那么对应的我们在提交参数开始执行任务时,就需要传入 List<String> 类型的参数了


List<String> list = new ArrayList<>();
list.add("薯条");
list.add("汉堡");
list.add("可乐");
new MyAsyncTask().execute(list);//这里若传入多个list,在doInBackground中按顺序取参即可

在 doInBackground 中获取我们提交的参数


protected String doInBackground(List<String>... params) {
for (String str : params[0])//params[0]就是我们提交的第一个list
...
return foods;//String foods
}

任务执行完毕后,返回一个 String 类型的值,该值即为 onPostExecute 方法的传入参数


protected void onPostExecute(String s)//s就是我们需要的返回值

好了,代码的细节分析完毕,下面我们来看看如何实现任务的进度更新功能


首先我们需要重写 AsyncTask.onProgressUpdate 回调方法,将更新进度UI的操作放在这里面


public class MyAsyncTask extends AsyncTask<List<String>,String,String>{
//省略部分代码...
@Override
protected void onProgressUpdate(String... values) {
super.onProgressUpdate(values);
textShow.setText(values[0]);
}
}

然后在 doInBackground 方法中( 子线程 )调用 publishProgress 方法就可以把当前任务进度传递到 onProgressUpdate 中( 主线程 )了


@Override
protected String doInBackground(List<String>... params) {
String foods = "已经配好的食物 —— ";
try {
for (String str : params[0]){
Thread.sleep(1000);//模拟配餐的时间
foods = foods + str + " ";
publishProgress(foods);//同样这里可以传递多个String类型的参数
}
Thread.sleep(500);
}catch (Exception e){}
return foods;
}

运行结果如下





除了以上这些方法外, AsyncTask 还提供了 onCancelled 回调方法。当我们调用 cancel(true) 时, doInBackground 方法将强制 中断任务 并调用 onCancelled ( onCancelled 被调用时 onPostExecute 不会被调用),因此我们可以将取消任务的操作放在 onCancelled 中,例如


public class MyAsyncTask extends AsyncTask<List<String>,String,String>{
//省略部分代码...
@Override
protected void onCancelled(String s) {
super.onCancelled(s);
textShow.setText("未完成配餐," + s);
}
@Override
protected String doInBackground(List<String>... params) {
String foods = "已经配好的食物 —— ";
try {
for (String str : params[0]){
if(str.equals("可乐")){
Thread.sleep(500);
cancel(true);
while (true){
/**
* cancel方法只是简单把标志位改为true
* 最后使用Thread.interrupt去中断线程执行
* 但这不能保证可以马上停止任务
* 所以需使用isCancelled来判断任务是否被取消
*/
if(isCancelled()){
return foods;
}
}
}
...
}
Thread.sleep(500);
}catch (Exception e){}
return foods;
}
}

运行结果如下





简单总结一下这些 AsyncTask 中常用的方法


execute (Params... params):提交参数, 开始任务
onPreExecute (): 执行任务之前 的准备操作
doInBackground (Params... params):在 子线程 中执行任务, 返回任务结果
onPostExecute (Result result): 接收任务结果 ,在 UI线程 ( 主线程 )中执行相应操作
onProgressUpdate (Progress... values):在 UI线程 中执行 更新进度 的操作,配套的 提交任务进度 的方法为 publishProgress (Progress... values)
onCancelled(Result result) :接收 取消任务时的结果 并执行相应的操作,配套的 取消中断任务 的方法为 cancel (boolean mayInterruptIfRunning)

那么这些方法在 AsyncTask 内部具体是怎么运作的呢?下面我们就继续深入探寻一番吧


AsyncTask内部工作流程

之前我们简单地总结过一次流程:


开始任务( execute ) → 任务准备 ( onPreExecute ) → 执行任务 ( doInBackground ) → 反馈任务结果 → 回到主线程执行相应操作 ( onPostExecute )


如果将这个流程继续细化,则如下图所示





从图中我们可以看到 线程池ThreadPoolExecutor ,负责 线程间通信的Handler 等等。如果有看过我之前几篇博客或者了解过相关知识的童鞋应该很快就能在脑中描绘出 AsyncTask 整个工作流程的蓝图了。我们这里就不一行行地分析每个方法的源码了,只是对照着上图帮大家理清思路,这样大家去看一些源码分析的博客时就没那么头疼了


首先从实现 AsyncTask 的子类说起, AsyncTask 内部有3个状态,它们封装在 Status 枚举类中,分别是


Status.PENDING :在 AsyncTask 对象创建时就设置了状态为 PENDING ,表示 AsyncTask 等待被使用,尚未开始执行任务
Status.RUNNING :提交参数开始任务后,状态设置为 RUNNING ,表示 AsyncTask 处于执行任务的状态,任务正在运行中
Status.FINISHED :任务完成后,状态会设置成 FINISHED

需要补充的是,这些状态在整个 任务生命周期 中只会 设置一次 ,何时设置状态已在上图用 虚线 标出


我们调用 execute 方法后, AsyncTask 会继续调用 executeOnExecutor 方法(在此方法中调用了 onPreExecute ,因此在 创建子线程执行任务前 就完成了 准备操作 )并传入默认的任务执行者 SERIAL_EXECUTOR ( SerialExecutor ),在 SerialExecutor 中维护着一个 任务队列 并限制了任务必须 单一依次执行 。很多博客将 SerialExecutor 说成是一个线程池,我个人并不赞同这一说法,因为实际上在 SerialExecutor 中完成创建线程、维护线程这些工作的是一个真正意义上的线程池( THREAD_POOL_EXECUTOR ),因此最终提交任务的操作还是回到了 线程池 的老路子,调用 ThreadPoolExecutor.execute 方法将任务入列


Android 7.0版本的 AsyncTask 默认 串行执行任务 ,那有什么方法可以突破这一限制呢?答案是调用我们之前提到的 executeOnExecutor 方法


public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params)

我们可以跳过 execute 方法直接调用 executeOnExecutor 并传入我们 自定义的线程池 ,这样就可以 并发 地执行多线程任务了


回到我们的工作流程,之前讲到调用 ThreadPoolExecutor.execute 方法提交任务,提交的任务类型为 Callable ( WorkerRunnable ), AsyncTask 在其 call 方法中调用 doInBackground 方法。也就是说,提交任务后, ThreadPoolExecutor 创建了 子线程 ,而 子线程 执行了 doInBackground 中的耗时任务


任务执行完毕后,按套路使用 Handler 发送 Message 通知 主线程 耗时任务已经完成了(或调用 publishProgress 方法一样可以让 Handler 发送消息通知 主线程 执行 更新进度 的操作),之后的事件就是根据发送 Message 的内容决定是执行 onPostExecute (若设置了任务取消则执行 onCancelled )还是 onProgressUpdate 方法了(上图由于位置有限无法体现 更新进度 这一过程,原理实际上是一样的)


那么到这 AsyncTask 的内部工作流程我们已经基本过了一遍,如果想要更深入地了解源码实现的过程,这里向大家推荐几位前辈的博客(由于他们博客更新的时间不同,大家需注意比对各版本 AsyncTask 的差异)


Android AsyncTask 源码解析 Android 7.0 AsyncTask分析 以及Android进阶之光 一书中关于 AsyncTask 的讲解


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台