探索 Android 多线程 - 1 AsyncTask
前言
在 Thinking In Java 中,详尽的介绍了基于 Java 的多线程开发的基础知识。笔者也之前做了相应的读书笔记,就不再本篇中赘诉。没有完全掌握的读者可以查看之前的博客了解这些基础的知识。本篇主要讲的是多线程技术在 Android 开发的运用。
Android 使用多线程的场景
在单核处理器中使用多线程的最多的是因为阻塞,使用为了减少 CPU 的等待时间。而在 Android 上,更明显的体现就是在主线程调用耗时的操作时,UI 会出现掉帧甚至卡死的情况。
为了保持应用良好的流畅度,其中的一个方式就是将不必在主线程执行的工作异步到其他线程执行。
Ax6dF8K.jpg
Android 多线程工具类
Android 系统为我们提供了以下四种方便进行异步操作的工具类:
- AsyncTask : 为 UI 线程与工作线程之间进行快速的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。
- HandlerThread : 为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
- ThreadPool : 把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。
- IntentService : 适合于执行由 UI 触发的后台 Service 任务,并可以把后台任务执行的情况通过一定的机制反馈给 UI。
ThreadPool 就是我们在 并发(1) -- 线程与线程池 中讲到的 集中 Executor, 本篇我们主要介绍的是 AsyncTask 和 HandlerThread。
AsyncTask 的使用
AsyncTask 封装好线程切换的细节,用三个范型类型来定义异步任务的入参,进度单位,返回结果。无需显式的进行线程创建和切换操作,使用时只需要重写对应的方法就可以实现异步任务执行前,执行时,执行结果处理等功能。
gRpR5vN.jpg下面我们模拟一个下载的异步任务:
/**
* AsyncTask 的三个范型类型分别为 Params, Progress, Result
* 代表着 doInBackground 的入参,异步任务的进度单位和,doInBackground 的出参
*
*/
public class DownloadTask extends AsyncTask<String, Integer, String> {
/**
* 任务开始之前调用,用于界面上的初始化操作,比如显示进度条的 dialog
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
}
/**
* 在子线程上执行需要异步处理的任务,无法跟直接进行 UI 操作
* 在该方法内调用 publishProgress(Progress...) 方法,触发 onProgressUpdate
* @param strings
* @return
*/
@Override
protected String doInBackground(String... strings) {
// TODO: 2018/9/19 根据 uri 下载到sd卡中,并返回文件位置
return null;
}
/**
* 在 doInBackground 方法中调用 publishProgress 时,可以进行 UI 操作
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
/**
* 在任务结束时调用,可用于做提示或关闭进度条的 dialog 等操作
* @param s
*/
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
}
}
// 使用方式
new DownloadTask(uri).excute();
我们通过以上方法,了解了 AsyncTask 任务执行和执行前后 UI 操作的方法的使用。然而,在体验高度封装的便利的同时,也需要注意它所带来的隐患。
- 串行执行任务
我们查看 excute() 的源码,发现最终调用到默认的执行器中(SerialExecutor)。AsyncTask 将每次执行的任务转化为一个链式调用的runnable,然后添加到一个 deque 中。
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();//使用 deque 存储 excute() 所执行的任务
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_PO![gRpR5vN.jpg](https://img.haomeiwen.com/i4894808/a913bc656ecec4a5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
OL_EXECUTOR.execute(mActive);
}
}
}
由此我们可以知道任务是串行队列,执行的流程如下图所示,。所以在频繁使用 AsyncTask 时,会导致队伍后面的任务处于阻塞状态。
AtjqrwF.jpg而为了解决 Task 串行存在的等待问题,可以使用 AsyncTask.executeOnExecutor
为其指定一个并行的执行器,使 AsyncTask 并行处理任务。
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 5; i++) {
new DownloadTask().executeOnExecutor(executorService);
}
IallFdi.jpg
- 是否能正确的 cancel
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
根据cancel()
方法的源码,我们可以了解到。一旦任务被成功中止,AsyncTask 就不会继续调用 onPostExecute()
,而是通过调用 onCancelled()
的回调方法反馈任务执行取消的结果。
我们可以根据任务回调到哪个方法(是 onPostExecute()
还是 onCancelled()
)来决定是对 UI 进行正常的更新还是把对应的任务所占用的内存进行销毁等。
而 AsyncTask 的取消机制本质上就是 FutureTask 的取消机制,同样不能保证任务在任何的状态下都能被成功的取消。为了保证任务在运行时更早的被取消,我们需要在doInBackground()
的代码中不断的添加程序isCancelled()
(是否被中止)的判断逻辑。
- 内存泄漏
在使用 AsyncTask 的过程中,很容易的把它定义为 Activity 的内部类,由于 AsyncTask 生命周期不稳定,可能会导致 Activity 的泄漏。由于 cancel 的不正确性,即使是在 Activity 的onDestroy()
中取消 AsyncTask 也无法保证避免泄漏的发生。
@Override
protected void onDestroy() {
super.onDestroy();
if (execute != null && !execute.isCancelled()) {
execute.cancel(true);// 无法正确取消
}
}
最后
引用:
网友评论