源码: github.com/AnliaLee
大家要是看到有错误的地方或者有啥好的建议, 欢迎留言评论
前言
本章我们将结合之前几篇博客, 来研究研究多线程知识综合应用程度很高的 AsyncTask 类(Android 7.0 版本)
大话 Android 多线程(一) Thread 和 Runnable 的联系和区别
大话 Android 多线程(二) synchronized 使用解析
大话 Android 多线程(三) 线程间的通信机制之 Handler
大话 Android 多线程(四) CallableFuture 和 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 的讲解
一些额外的补充
AsyncTask 不同版本线程池的区别
AsyncTask 的缺陷
相关博客推荐
AsyncTask 的缺陷和问题
Android 多线程 - AsyncTask 的使用和问题(取消, 并行和串行, 屏幕切换)
Android 实现弱引用 AsyncTask, 将内存泄漏置之度外
大话 Android 多线程系列到这就暂告一段落了, 不知道大家看完这个系列之后收获如何呢? 如果觉得还行那就多多点赞, 然后再买点橘子给博主吃当然, 我就吃两个, 剩下的都留给你们哈哈~
来源: https://juejin.im/post/5a85a6066fb9a06337573955