写点什么

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

评论

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

《迅雷链精品课》第五课:账户与账本

迅雷链

区块链

架构师Week5总结

lggl

总结

SQL数据库:GROUPING运算符

正向成长

GROUPING运算符

力扣(Leetcode)练习--给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序

Wynne

亚马逊全球百万钜惠引爆“黑五” 跨境狂欢“巅峰6日”震撼登场

爱极客侠

深入浅出 Go - sync.Once 源码分析

helbing

Go 语言

架构师训练营第九周作业

我是谁

极客大学架构师训练营

【云图说】第189期 初识数据仓库服务

华为云开发者联盟

数据库 数据仓库 数据

UNISKIN COO Kevin|营销数字化:数据沉淀和数据系统化运营一定要趁早!

Linkflow

营销数字化 客户数据平台 CDP

《Python程序员面试算法宝典》PDF 超清版免费领取

计算机与AI

Python 面试 算法

面试题总结--HashMap、Volatile相关

彭阿三

一致性hash算法

天涯若海

甲方日常 54

句子

工作 随笔杂谈 日常

MySQL主从数据库没有同步怎么办?

冰河

MySQL 数据库 分布式 微服务

公众号高频被调整,它不是企业生产文章的机器

Linkflow

客户数据平台 CDP 私域流量

强化学习入门必看之强化学习导识

Alocasia

人工智能 学习

JVM入门,认识Class文件

Simon郎

JVM Java 分布式

阿里作为内部参考的Redis文档现在开放下载,姐夫半夜不睡都在看

小Q

Java redis 学习 编程 面试

深入理解h2和r2dbc-h2

程序那些事

响应式编程 R2DBC 程序那些事 响应式架构 r2dbc-h2

粉丝求助:JAVA程序员,4年了,很迷茫,希望前辈可以给指出一个技术路线和需掌握的知识技能树;

Java架构师迁哥

Istio 1.8 发布——用户至上的选择

Jimmy Song

开源 云原生 Service Mesh istio

某美团程序员爆料:筛选简历时,用go语言的基本不看!网友:当韭菜还当出优越感了!

Java架构师迁哥

架构师Week5作业

lggl

作业

LAXCUS大数据集群操作系统挖矿

陈泽云

大数据 分布式计算 挖矿

涛涌天际,水利万物:黄浦江畔读懂城市智能体

脑极体

京东开发4年,想要跳槽去拼多多,落泪四4面,这年头跳槽可真难啊(还好不是裸辞)

Java~~~

架构 面试 编程语言 java架构师

如何在 vuePress中添加博客导流公众号-即输入验证码解锁全站文章

itclanCoder

vuepress 解锁文章 博客引流 建站

OpenFeign和Consul爱恨交织的两天

编号94530

Spring Cloud Consul OpenFegin spring 5

KubeVela 正式开源:一个高可扩展的云原生应用平台与核心引擎

阿里巴巴云原生

阿里云 开源 Kubernetes 云原生 OAM

苹果首发ARM架构电脑芯片,将对PC格局带来哪些影响?

脑极体

深入浅出 Go - sync.Map 源码分析

helbing

Go 语言

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