最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

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:584969

评论

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

Surpass Day——Java语法基础

胖虎不秃头

Java 10月月更 se

OpenMLDB Meetup No.5 会议纪要

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

Redis--Redis集群、缓存穿透、缓存击穿、缓存雪崩

Java学术趴

10月月更

活动紧急插播 | QCon 全球软件开发者大会早班车连线分享

第四范式开发者社区

人工智能 数据库 开源 时序数据库 特征

深度解密 OpenMLDB 毫秒级实时在线特征计算引擎

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

TiKV缩容下线异常处理的三板斧

TiDB 社区干货传送门

实践案例 管理与运维 故障排查/诊断 扩/缩容

开源星「001号」落地 OpenMLDB,欢迎登陆赢神秘大礼包!

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

首届第四范式 Tech Day 回顾

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

究竟都是谁在使用?OpenMLDB 落地案例大起底

第四范式开发者社区

人工智能 数据库 开源 时序数据库 特征

OpenMLDB + OneFlow: 手把手教你快速链接特征工程到模型训练

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

直播预告 | 第四范式Tech Day火热来袭,OpenMLDB 与你 8月11日线上见

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

直冲云霄,阿里大牛耗时49天整理12W字面试手册,押题准确率直冲95%

Geek_0c76c3

Java 数据库 开源 程序员 开发

倒数三天 | WAIC 滴水湖 AI 开发者创新论坛:当数据库遇上 AI 来啦!

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

打造友邻式多元生态,支撑工商银行、平安科技、中国人寿财险、杭州银行的创新实践

TiDB 社区干货传送门

数据库前沿趋势

谈谈产品如何定位

产品海豚湾

产品经理 产品定位 商业洞察 10月月更 SaaS 产品

专访 | 赵沁雪:参与开源,不是一个人的战斗

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

专访 | 许伟 ——贡献榜 Top4 也只是“开源小白”

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

AICon 2022 | 来杭州和 OpenMLDB 见面吧

第四范式开发者社区

人工智能 机器学习 数据库 时序数据库 特征

OpenMLDB+Byzer,SQL 也能玩转机器学习全流程

第四范式开发者社区

机器学习 数据库 开源 时序数据库 特征

OpenMLDB Airflow Connector:让 MLOps 工作流更自由

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

秋招Java面试大纲:Java+并发+spring+数据库+Redis+JVM+Netty等

Geek_0c76c3

Java 数据库 开源 程序员 开发

决策 AI:以高效落地为目标的工程技术

第四范式开发者社区

机器学习 数据库 时序数据库 特征

如何选择架构中的底层工具?OpenMLDB 在 Akulaku 数据驱动中的应用实践给你答案

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

Surpass Day——Java面向对象的封装、构造方法

胖虎不秃头

Java 10月月更 se

拼搏一周!刷了1000道Java高频面试题喜提阿里offer,定级P7

Geek_0c76c3

Java 数据库 开源 程序员 架构

OpenMLDB 开源一周年,感恩遇见

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

Surpass Day——Java面向对象的创建和使用

胖虎不秃头

Java 10月月更 se

刘奇:能否掌控复杂性,决定着分布式数据库的生死存亡

TiDB 社区干货传送门

数据库前沿趋势

OpenMLDB v0.6.0 发布 | 便捷性、可用性大幅提升!

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

大数据ELK(十五):Elasticsearch SQL简单介绍

Lansonli

ES 10月月更

TiDB 数据冷热存储分离测试

TiDB 社区干货传送门

实践案例 管理与运维 新版本/特性解读 数据库架构设计

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