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

2,524 阅读10分钟

版权声明:本文为博主原创文章,未经博主允许不得转载
源码:github.com/AnliaLee
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言

本章我们将结合之前几篇博客,来研究研究多线程知识综合应用程度很高的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方法将强制中断任务并调用onCancelledonCancelled被调用时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_EXECUTORSerialExecutor),在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方法提交任务,提交的任务类型为CallableWorkerRunnable),AsyncTask在其call方法中调用doInBackground方法。也就是说,提交任务后,ThreadPoolExecutor创建了子线程,而子线程执行了doInBackground中的耗时任务

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

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

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


一些额外的补充

  1. AsyncTask不同版本线程池的区别
  2. AsyncTask的缺陷

    相关博客推荐
    AsyncTask的缺陷和问题
    Android多线程-AsyncTask的使用和问题(取消,并行和串行,屏幕切换)

  3. Android实现弱引用AsyncTask,将内存泄漏置之度外

大话Android多线程系列到这就暂告一段落了,不知道大家看完这个系列之后收获如何呢?如果觉得还行那就多多点赞,然后再买点橘子给博主吃。当然,我就吃两个,剩下的都留给你们哈哈~

最后祝大家新春快乐"狗"到最后事事都旺旺~