产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

关于 Android 中工作者线程的思考

  • 2015-12-16
  • 本文字数:4349 字

    阅读完需:约 14 分钟

本文系 2015 北京 GDG Devfest 分享内容整理。

在 Android 中,我们或多或少使用了工作者线程,比如 Thread,AsyncTask,HandlerThread,甚至是自己创建的线程池,使用工作者线程我们可以将耗时的操作从主线程中移走。然而在 Android 系统中为什么存在工作者线程呢,常用的工作者线程有哪些不易察觉的问题呢,关于工作者线程有哪些优化的方面呢,本文将一一解答这些问题。

工作者线程的存在原因

  • 因为 Android 的 UI 单线程模型,所有的 UI 相关的操作都需要在主线程 (UI 线程) 执行
  • Android 中各大组件的生命周期回调都是位于主线程中,使得主线程的职责更重
  • 如果不使用工作者线程为主线程分担耗时的任务,会造成应用卡顿,严重时可能出现 ANR(Application Not Responding), 即程序未响应。

因而,在 Android 中使用工作者线程显得势在必行,如一开始提到那样,在 Android 中工作者线程有很多,接下来我们将围绕 AsyncTask,HandlerThread 等深入研究。

AsyncTask

AsyncTask 是 Android 框架提供给开发者的一个辅助类,使用该类我们可以轻松的处理异步线程与主线程的交互,由于其便捷性,在 Android 工程中,AsyncTask 被广泛使用。然而 AsyncTask 并非一个完美的方案,使用它往往会存在一些问题。接下来将逐一列举 AsyncTask 不容易被开发者察觉的问题。

AsyncTask 与内存泄露

内存泄露是 Android 开发中常见的问题,只要开发者稍有不慎就有可能导致程序产生内存泄露,严重时甚至可能导致 OOM(OutOfMemory,即内存溢出错误)。AsyncTask 也不例外,也有可能造成内存泄露。

以一个简单的场景为例:
在 Activity 中,通常我们这样使用 AsyncTask

复制代码
//In Activity
new AsyncTask<String, Void, Void>() {
@Override
protected Void doInBackground(String... params) {
//some code
return null;
}
}.execute("hello world");

上述代码使用的匿名内存类创建 AsyncTask 实例,然而在 Java 中,非静态内存类会隐式持有外部类的实例引用,上面例子 AsyncTask 创建于 Activity 中,因而会隐式持有 Activity 的实例引用。

而在 AsyncTask 内部实现中,mFuture 同样使用匿名内部类创建对象,而 mFuture 会作为执行任务加入到任务执行器中。

复制代码
private final WorkerRunnable<Params, Result> mWorker;
public AsyncTask() {
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
//some code
}
};
}

而 mFuture 加入任务执行器,实际上是放入了一个静态成员变量 SERIAL_EXECUTOR 指向的对象 SerialExecutor 的一个 ArrayDeque 类型的集合中。

复制代码
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
//fake code
r.run();
}
});
}
}

当任务处于排队状态,则 Activity 实例引用被静态常量 SERIAL_EXECUTOR 间接持有。

在通常情况下,当设备发生屏幕旋转事件,当前的 Activity 被销毁,新的 Activity 被创建,以此完成对布局的重新加载。

而本例中,当屏幕旋转时,处于排队的 AsyncTask 由于其对 Activity 实例的引用关系,导致这个 Activity 不能被销毁,其对应的内存不能被 GC 回收,因而就出现了内存泄露问题。

关于如何避免内存泄露,我们可以使用静态内部类 + 弱引用的形式解决。

cancel 的问题

AsyncTask 作为任务,是支持调用者取消任务的,即允许我们使用 AsyncTask.canncel() 方法取消提交的任务。然而其实 cancel 并非真正的起作用。

首先,我们看一下 cancel 方法:

复制代码
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}

cancel 方法接受一个 boolean 类型的参数,名称为mayInterruptIfRunning,意思是是否可以打断正在执行的任务。

当我们调用 cancel(false),不打断正在执行的任务,对应的结果是

  • 处于 doInBackground 中的任务不受影响,继续执行
  • 任务结束时不会去调用onPostExecute方法,而是执行onCancelled方法

当我们调用 cancel(true),表示打断正在执行的任务,会出现如下情况:

  • 如果 doInBackground 方法处于阻塞状态,如调用 Thread.sleep,wait 等方法,则会抛出 InterruptedException。
  • 对于某些情况下,有可能无法打断正在执行的任务

如下,就是一个 cancel 方法无法打断正在执行的任务的例子

复制代码
AsyncTask<String,Void,Void> task = new AsyncTask<String, Void, Void>() {
@Override
protected Void doInBackground(String... params) {
boolean loop = true;
while(loop) {
Log.i(LOGTAG, "doInBackground after interrupting the loop");
}
return null;
}
}
task.execute("hello world");
try {
Thread.sleep(2000);// 确保 AsyncTask 任务执行
task.cancel(true);
} catch (InterruptedException e) {
e.printStackTrace();
}

上面的例子,如果想要使 cancel 正常工作需要在循环中,需要在循环条件里面同时检测isCancelled()才可以。

串行带来的问题

Android 团队关于 AsyncTask 执行策略进行了多次修改,修改大致如下:

  • 自最初引入到 Donut(1.6) 之前,任务串行执行
  • 从 Donut 到 GINGERBREAD_MR1(2.3.4), 任务被修改成了并行执行
  • 从 HONEYCOMB(3.0)至今,任务恢复至串行,但可以设置executeOnExecutor()实现并行执行。

然而 AsyncTask 的串行实际执行起来是这样的逻辑

  • 由串行执行器控制任务的初始分发
  • 并行执行器一次执行单个任务,并启动下一个

在 AsyncTask 中,并发执行器实际为 ThreadPoolExecutor 的实例,其 CORE_POOL_SIZE 为当前设备 CPU 数量 +1,MAXIMUM_POOL_SIZE 值为 CPU 数量的 2 倍 + 1。

以一个四核手机为例,当我们持续调用 AsyncTask 任务过程中

  • 在 AsyncTask 线程数量小于 CORE_POOL_SIZE(5 个) 时,会启动新的线程处理任务,不重用之前空闲的线程
  • 当数量超过 CORE_POOL_SIZE(5 个),才开始重用之前的线程处理任务

但是由于 AsyncTask 属于默认线性执行任务,导致并发执行器总是处于某一个线程工作的状态,因而造成了 ThreadPool 中其他线程的浪费。同时由于 AsyncTask 中并不存在 allowCoreThreadTimeOut(boolean) 的调用,所以 ThreadPool 中的核心线程即使处于空闲状态也不会销毁掉。

Executors

Executors 是 Java API 中一个快速创建线程池的工具类,然而在它里面也是存在问题的。

以 Executors 中获取一个固定大小的线程池方法为例

复制代码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L,
TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

在上面代码实现中,CORE_POOL_SIZE 和 MAXIMUM_POOL_SIZE 都是同样的值,如果把 nThreads 当成核心线程数,则无法保证最大并发,而如果当做最大并发线程数,则会造成线程的浪费。因而 Executors 这样的 API 导致了我们无法在最大并发数和线程节省上做到平衡。

为了达到最大并发数和线程节省的平衡,建议自行创建 ThreadPoolExecutor,根据业务和设备信息确定 CORE_POOL_SIZE 和 MAXIMUM_POOL_SIZE 的合理值。

HandlerThread

HandlerThread 是 Android 中提供特殊的线程类,使用这个类我们可以轻松创建一个带有 Looper 的线程,同时利用 Looper 我们可以结合 Handler 实现任务的控制与调度。以 Handler 的 post 方法为例,我们可以封装一个轻量级的任务处理器

复制代码
private Handler mHandler;
private LightTaskManager() {
HandlerThread workerThread = new HandlerThread("LightTaskThread");
workerThread.start();
mHandler = new Handler(workerThread.getLooper());
}
public void post(Runnable run) {
mHandler.post(run);
}
public void postAtFrontOfQueue(Runnable runnable) {
mHandler.postAtFrontOfQueue(runnable);
}
public void postDelayed(Runnable runnable, long delay) {
mHandler.postDelayed(runnable, delay);
}
public void postAtTime(Runnable runnable, long time) {
mHandler.postAtTime(runnable, time);
}

在本例中,我们可以按照如下规则提交任务

  • post 提交优先级一般的任务
  • postAtFrontOfQueue 将优先级较高的任务加入到队列前端
  • postAtTime 指定时间提交任务
  • postDelayed 延后提交优先级较低的任务

上面的轻量级任务处理器利用 HandlerThread 的单一线程 + 任务队列的形式,可以处理类似本地 IO(文件或数据库读取)的轻量级任务。在具体的处理场景下,可以参考如下做法:

  • 对于本地 IO 读取,并显示到界面,建议使用 postAtFrontOfQueue
  • 对于本地 IO 写入,不需要通知界面,建议使用 postDelayed
  • 一般操作,可以使用 post

线程优先级调整

在 Android 应用中,将耗时任务放入异步线程是一个不错的选择,那么为异步线程调整应有的优先级则是一件锦上添花的事情。众所周知,线程的并行通过 CPU 的时间片切换实现,对线程优先级调整,最主要的策略就是降低异步线程的优先级,从而使得主线程获得更多的 CPU 资源。

Android 中的线程优先级和 Linux 系统进程优先级有些类似,其值都是从 -20 至 19。其中 Android 中,开发者可以控制的优先级有:

  • THREAD_PRIORITY_DEFAULT,默认的线程优先级,值为 0
  • THREAD_PRIORITY_LOWEST,最低的线程级别,值为 19
  • THREAD_PRIORITY_BACKGROUND 后台线程建议设置这个优先级,值为 10
  • THREAD_PRIORITY_MORE_FAVORABLE 相对THREAD_PRIORITY_DEFAULT稍微优先,值为 -1
  • THREAD_PRIORITY_LESS_FAVORABLE 相对THREAD_PRIORITY_DEFAULT稍微落后一些,值为 1

为线程设置优先级也比较简单,通用的做法是在 run 方法体的开始部分加入下列代码

android.os.Process.setThreadPriority(priority);通常设置优先级的规则如下:

  • 一般的工作者线程,设置成THREAD_PRIORITY_BACKGROUND
  • 对于优先级很低的线程,可以设置THREAD_PRIORITY_LOWEST
  • 其他特殊需求,视业务应用具体的优先级

总结

在 Android 中工作者线程如此普遍,然而潜在的问题也不可避免,建议在开发者使用工作者线程时,从工作者线程的数量和优先级等方面进行审视,做到较为合理的使用。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015-12-16 17:297180

评论

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

win版HD Video Converter Factory Pro(高清视频转换器) v27.6特别版

iMac小白

会议通知 | 大模型算力平台标准研讨会即将召开

中国信通院AI Infra工作组

介绍一下链游GameFi的玩法和类型有哪些

区块链开发团队DappNetWork

主动元数据平台详解(下):BIG 十一问,详解定位、对接、血缘保鲜等问题

Aloudata

数据管理 数据血缘 主动元数据

链游Gamefi,NFT游戏都有什么特点或优势,能吸引玩家呢

区块链开发团队DappNetWork

达梦公开上市,首日收盘市值达182亿;Oracle云上将支持Oracle Database@Google Cloud

NineData

oracle Google 国产数据库 达梦 数据库上市

数据管理优化,支持从 API 和对象存储更新数据集|ModelWhale 版本更新

ModelWhale

人工智能 对象存储 模型训练 数据集 API

和鲸科技携手浙江大学地球科学学院,助推地球科学研究范式变革

ModelWhale

战略合作 地球科学 浙江大学 重点实验室

一步一步教你用 Python 的 Requests 库发送 JSON 数据

Apifox

Python json 程序员 后端 API

观测云产品更新 | BPF 网络日志、智能监控、告警策略等

观测云

win版IObit Driver Booster Pro(驱动管理工具) v11.5.0.85便携版

iMac小白

win版Gihosoft TubeGet Pro(YouTube 视频下载工具) v9.3.88 专业激活版

iMac小白

win版Adobe Lightroom Classic 2024(LrC2024) v13.3.1.1(x64)直装版

iMac小白

win版DVD-Cloner 2024(DVD复制软件)特别版

iMac小白

探索国产化项目管理工具:6款顶尖系统全面对比

爱吃小舅的鱼

项目管理 系统 国产

win版IObit Driver Booster Pro(驱动更新软件) v11.5.0.85 特别版

iMac小白

win版GoodSync(文件同步备份工具) v12.6.8.8便携版

iMac小白

基础大模型纵横天下,垂直大模型各领风骚,专注一域称尊

松子(李博源)

大数据 大模型 agent 职场影响

打破数据孤岛,驱动车企增长--数造科技网联大数据平台实战解析

数造万象

车联网 数字化 案例分享 汽车 制造业

链游Gamefi,NFT游戏都有什么特点或优势,能吸引玩家呢

区块链开发团队DappNetWork

【YashanDB知识库】PHP使用ODBC使用数据库绑定参数功能异常

YashanDB

数据库系统 yashandb 崖山数据库

拆解元宇宙:什么是链游gamefi,nft游戏

区块链开发团队DappNetWork

DAPP系统开发 元宇宙游戏 区块链开发DAPP开发 区块链kaifa链游开发 链游开发dapp开发

介绍一下链游GameFi的玩法和类型有哪些

区块链开发团队DappNetWork

链游gamefi,NFT游戏经济模型定制设计开发

区块链开发团队DappNetWork

win版PanoramaStudio Pro(全景图像制作工具) v4.0.6.413 激活版

iMac小白

精彩收官!北京中医药大学第五届“和鲸杯”医学数据分析大赛落幕

ModelWhale

人工智能 机器学习 数据分析 数据科学竞赛

win版R-Wipe & Clean(磁盘清理) v20.0.2461 激活版

iMac小白

天猫搜索API返回值解析:关键字搜索在电商市场趋势预测中的作用

技术冰糖葫芦

API Explorer API 安全 API 文档 API 开发 pinduoduo API

关于个人信息安全,这68项标准不容错过!(附下载)

极盾科技

数据安全

关于Android中工作者线程的思考_Android/iOS_段建华_InfoQ精选文章