写点什么

Android 异步类 AsyncTask 详解

  • 2015-06-09
  • 本文字数:5661 字

    阅读完需:约 19 分钟

在 Android 应用开发的过程中,我们需要时刻注意保证应用程序的稳定和 UI 操作响应及时,因为不 稳定或响应缓慢的应用将给应用带来不好的印象,严重的用户卸载你的 APP,这样你的努力就没有体现的价值了。本文试图从 AsnycTask 的作用说起,进 一步的讲解一下内部的实现机制。如果有一些开发经验的人,读完之后应该对使用 AsnycTask 过程中的一些问题豁然开朗,开发经验不丰富的也可以从中找 到使用过程中的注意点。

为何引入 AsnyncTask?

在 Android 程序开始运行的时候会单独启动一个进程,默认情况下所有这个程序操作都在这个进程中进行。一个 Android 程序默认情况下只有一个进程,但是一个进程却是可以有许多线程的。

在这些线程中,有一个线程叫做 UI 线程,也叫做 Main Thread,除了 Main Thread 之外的线程都可称为 Worker Thread。Main Thread 主要负责控制 UI 页面的显示、更新、交互等。因此所有在 UI 线程中的操作要求越短越好,只有这样用户才会觉得操作比较流畅。一个比较好的做法 是把一些比较耗时的操作,例如网络请求、数据库操作、复杂计算等逻辑都封装到单独的线程,这样就可以避免阻塞主线程。为此,有人写了如下的代码:

复制代码
private TextView textView;
public void onCreate(Bundle bundle){
super.onCreate(bundle);
setContentView(R.layout.thread_on_ui);
textView = (TextView) findViewById(R.id.tvTest);
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpGet httpGet = new HttpGet("http://www.baidu.com");
HttpClient httpClient = new DefaultHttpClient();
HttpResponse httpResp = httpClient.execute(httpGet);
if (httpResp.getStatusLine().getStatusCode() == 200) {
String result = EntityUtils.toString(httpResp.getEntity(), "UTF-8");
textView.setText(" 请求返回正常,结果是:" + result);
} else {
textView.setText(" 请求返回异常!");
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}

运行,不出所料,异常信息如下:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.怎么破?可以在主线程创建 Handler 对象,把 textView.setText 地方替换为用 handler 把返回值发回到 handler 所在的线程处理,也就是主线程。这个处理方法稍显复杂,Android 为我们考虑到了这个情况,给我们提供了 一个轻量级的异步类可以直接继承 AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的结果以及执行进度,这些接口中有直接运行在主线程 中的,例如 onPostExecute,onPreExecute 等方法。

也就是说,Android 的程序运行时是多线程的,为了更方便的处理子线程和 UI 线程的交互,引入了 AsyncTask。

AsnyncTask 内部机制

AsyncTask 内部逻辑主要有二个部分:

1、与主线的交互,它内部实例化了一个静态的自定义类 InternalHandler,这个类是继承自 Handler 的,在这个自定义类中绑定了一个叫做 AsyncTaskResult 的对象,每次子线程需 要通知主线程,就调用 sendToTarget 发送消息给 handler。然后在 handler 的 handleMessage 中 AsyncTaskResult 根据消息的类型不同(例如 MESSAGE_POST_PROGRESS 会更新进度 条,MESSAGE_POST_CANCEL 取消任务)而做不同的操作,值得一提的是,这些操作都是在 UI 线程进行的,意味着,从子线程一旦需要和 UI 线 程交互,内部自动调用了 handler 对象把消息放在了主线程了。源码地址

复制代码
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void More ...done() {
Message message;
Result result = null;
try {
result = get();
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occured while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
message = sHandler.obtainMessage(MESSAGE_POST_CANCEL,
new AsyncTaskResult<Result>(AsyncTask.this, (Result[]) null));
message.sendToTarget();
return;
} catch (Throwable t) {
throw new RuntimeException("An error occured while executing "
+ "doInBackground()", t);
}
message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(AsyncTask.this, result));
message.sendToTarget();
}
};
private static class InternalHandler extends Handler {
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void More ...handleMessage(Message msg) {
AsyncTaskResult result = (AsyncTaskResult) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
case MESSAGE_POST_CANCEL:
result.mTask.onCancelled();
break;
}
}
}

2、AsyncTask 内部调度,虽然可以新建多个 AsyncTask 的子类的实例,但是 AsyncTask 的内部 Handler 和 ThreadPoolExecutor 都是 static 的,这么定义的变 量属于类的,是进程范围内共享的,所以 AsyncTask 控制着进程范围内所有的子类实例,而且该类的所有实例都共用一个线程池和 Handler。代码如 下:

复制代码
public abstract class AsyncTask<Params, Progress, Result> {
private static final String LOG_TAG = "AsyncTask";
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;
private static final BlockingQueue<Runnable> sWorkQueue =
new LinkedBlockingQueue<Runnable>(10);
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread More ...newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
private static final int MESSAGE_POST_RESULT = 0x1;
private static final int MESSAGE_POST_PROGRESS = 0x2;
private static final int MESSAGE_POST_CANCEL = 0x3;

从代码还可以看出,默认核心线程池的大小是 5,缓存任务队列是 10。意味着,如果线程池的线程数量小 于 5,这个时候新添加一个异步任务则会新建一个线程;如果线程池的数量大于等于 5,这个时候新建一个异步任务这个任务会被放入缓存队列中等待执行。限制一 个 APP 内 AsyncTask 并发的线程的数量看似是有必要的,但也带来了一个问题,假如有人就是需要同时运行 10 个而不是 5 个,或者不对线程的多少做限 制,例如有些 APP 的瀑布流页面中的 N 多图片的加载。

另一方面,同时运行的任务多,线程也就多,如果这些任务是去访问网络的,会导致短时间内手机那可怜的带宽被占完了,这样总体的表现是谁都很难很快加载完全,因为他们是竞争关系。所以,把选择权交给开发者吧。

事实上,大概从 Android 从 3.0 开始,每次新建异步任务的时候 AsnycTask 内部默认规则是按提交的先后顺序每次只运行一个异步任务。当然了你也可以自己指定自己的线程池。

可以看出,AsyncTask 使用过程中需要注意的地方不少

  • 由于 Handler 需要和主线程交互,而 Handler 又是内置于 AsnycTask 中的,所以,AsyncTask 的创建必须在主线程。
  • AsyncTaskResult 的 doInBackground(mParams) 方法执行异步任务运行在子线程中,其他方法运行在主线程中,可以操作 UI 组件。
  • 不要手动的去调用 AsyncTask 的 onPreExecute, doInBackground, publishProgress, onProgressUpdate, onPostExecute 方法,这些都是由 Android 系统自动调用的
  • 一个任务 AsyncTask 任务只能被执行一次。
  • 运行中可以随时调用 cancel(boolean) 方法取消任务,如果成功调用 isCancelled() 会返回 true,并且不会执行 onPostExecute() 方法了,取而代之的是调用 onCancelled() 方法。而且从源码看,如果这个任务已经执行了这个时候调用 cancel 是不会真正的把 task 结束,而是继续执行,只不过改变的是执行之后的回调方法是 onPostExecute 还是 onCancelled。

AsnyncTask 和 Activity OnConfiguration

上面提到了那么多的注意点,还有其他需要注意的吗?当然有!我们开发 App 过程中使用 AsyncTask 请求网络数据的时候,一般都是习惯在 onPreExecute 显示进度条,在数据请求完成之后的 onPostExecute 关闭进度 条。这样做看似完美,但是如果您的 App 没有明确指定屏幕方向和 configChanges 时,当用户旋转屏幕的时候 Activity 就会重新启动,而这 个时候您的异步加载数据的线程可能正在请求网络。当一个新的 Activity 被重新创建之后,可能由重新启动了一个新的任务去请求网络,这样之前的一个异 步任务不经意间就泄露了,假设你还在 onPostExecute 写了一些其他逻辑,这个时候就会发生意想不到异常。

一般简单的数据类型的,对付 configChanges 我们很好处理,我们直接可以通过 onSaveInstanceState() 和 onRestoreInstanceState() 进行保存与恢复。Android 会在销毁你的 Activity 之前调用 onSaveInstanceState() 方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在 onCreate() 或 onRestoreInstanceState() 方法中恢复。

但是,对于 AsyncTask 怎么办?问题产生的根源在于 Activity 销毁重新创建的过程中 AsyncTask 和之前的 Activity 失联,最终导致一些问题。那么解决问题的思路也可以朝着这个方向发展。 Android 官方文档也有一些解决问题的线索。

这里介绍另外一种使用事件总线的解决方案,是国外一个安卓大牛写的。中间用到了 Square 开源的 EventBus 类库 http://square.github.io/otto/ 。首先自定义一个 AsyncTask 的子类,在 onPostExecute 方法中,把返回结果抛给事件总线,代码如下:

复制代码
@Override
protected String doInBackground(Void... params) {
Random random = new Random();
final long sleep = random.nextInt(10);
try {
Thread.sleep(10 * 6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Slept for " + sleep + " seconds";
}
@Override
protected void onPostExecute(String result) {
MyBus.getInstance().post(new AsyncTaskResultEvent(result));
}

在 Activity 的 onCreate 中注册这个事件总线,这样异步线程的消息就会被 otta 分发到当前注册的 activity,这个时候返回结果就在当前 activity 的 onAsyncTaskResult 中了,代码如下:

复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.otto_layout);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
new MyAsyncTask().execute();
}
});
MyBus.getInstance().register(this);
}
@Override
protected void onDestroy() {
MyBus.getInstance().unregister(this);
super.onDestroy();
}
@Subscribe
public void onAsyncTaskResult(AsyncTaskResultEvent event) {
Toast.makeText(this, event.getResult(), Toast.LENGTH_LONG).show();
}

个人觉的这个方法相当好,当然更简单的你也可以不用 otta 这个库,自己单独的用接口回调的方式估计也能实现,大家可以试试。

本文作者为 OneAPM 工程师张新勇,原文地址。本文已由作者方授权 InfoQ 中文站转载。

2015-06-09 07:585115

评论

发布
暂无评论
发现更多内容

fastposter v2.9.2 最简海报生成器

物有本末

海报生成器 电商海报 图片生成

软件测试 | 测试开发 | app自动化测试(Android)--高级定位技巧

测吧(北京)科技有限公司

xpath

助力企业成就好生意,华为云快成长直播

科技云未来

开源治理的基本实践与指导原则

SEAL安全

开源 开源安全 软件供应链安全 开源安全与治理

一线技术人应该关注的四种思维能力

阿里巴巴中间件

阿里云 技术文章

设计模式的艺术 第二十章中介者模式练习(设计一套图形界面类库,包含若干预定义的窗格(Pane)对象,如TextPane、ListPane等,窗格之间不允许直接引用。基于该类库的应用由一个包含一组窗格的窗口(Window)组成,窗口协调窗格之间的行为)

代廉洁

设计模式的艺术

从原理剖析带你理解Stream

华为云开发者联盟

开发 企业号九月金秋榜

为什么资源隔离对HTAP至关重要?

OceanBase 数据库

软件测试 | 测试开发 | 测试人生 | 薪资翻倍涨至50W是种什么样的体验?

测吧(北京)科技有限公司

测试

YOLOX-PAI:加速YOLOX,比YOLOV6更快更强

阿里云大数据AI技术

深度学习 模型优化 企业号九月金秋榜

易观分析&Pangle联合发布《全球新兴市场移动应用报告》

易观分析

新兴市场

开学季 | Y 省教育厅这张卷,融云答出了100分!

融云 RongCloud

通讯协议

TDengine3.0计算查询引擎的优化与升级

TDengine

数据库 tdengine 时序数据库 企业号九月金秋榜

软件测试 | 测试开发 | app自动化测试(Android)--触屏操作自动化

测吧(北京)科技有限公司

自动化测试 app测试

软件测试 | 测试开发 | Jenkins 集成 Android 代码检查

测吧(北京)科技有限公司

android jenkins

华为云快成长直播ERP专场,以数据驱动企业智慧变革

科技云未来

【译】YouTube 架构

Rae

数据库 架构 youtube 后端技术

你的秋日好运正在派件,请查收9月月更活动!

InfoQ写作社区官方

热门活动 9月月更

上了NVMe的路,才能飙起全闪存的车

脑极体

TiFlash 源码解读(七)TiFlash Proxy 模块

PingCAP

TiDB TiDB 源码解读

软件测试 | 测试开发 | AppCrawler 自动遍历测试实践(三):动手实操与常见问题汇总

测吧(北京)科技有限公司

测试

边缘服务网格 osm-edge 概览

Flomesh

Service Mesh 服务网格

软件测试 | 测试开发 | 疫情之下工资翻了2倍多,这4个月学习比工作8年学到的还多

测吧(北京)科技有限公司

软件测试

基于MonoRepo的Web端CI/CD实践与优化

RingCentral铃盛

企业号九月金秋榜

软件测试 | 测试开发 | 测试人生 | 双非学历入职名企大厂还薪资翻倍?

测吧(北京)科技有限公司

面试 测试

Python代码用在这些地方,其实1行就够了

华为云开发者联盟

Python 开发 企业号九月金秋榜

软件测试 | 测试开发 | 简单快速的从GitHub同步代码

测吧(北京)科技有限公司

git

软件测试 | 测试开发 | app自动化测试(Android)-- Capability 使用进阶

测吧(北京)科技有限公司

Andriod

软件测试 | 测试开发 | 用 Pytest+Allure 生成漂亮的 HTML 图形化测试报告

测吧(北京)科技有限公司

pytest Allure

虚拟机内存管理之内存分配器

字节跳动终端技术

vm 内存 虚拟机 内存管理 内存分配

软件测试 | 测试开发 | 常见接口协议解析

测吧(北京)科技有限公司

TCP

Android异步类AsyncTask详解_移动_张新勇_InfoQ精选文章