本文内容整理自公众号腾讯 Bugly, 感谢原作者的分享.
1, 问题背景
对于 Android 应用来说, 内存向来是比较重要的性能指标. 内存占用过高, 会影响应用的流畅度, 甚至引发 OOM, 非常影响用户体验. 因此, 内存优化也向来是行业内的重点工作项和难点工作项.
手Q在很早之前就开发了很多内存优化技术:
1)自研内存泄露检测系统 LeakInspector 天网:
LeakInspector 是一套完整内存泄露检测系统: 能够自动检测应用内存泄露问题; 并提供兜底回收以及自动提单功能;
2)图片引用大图告警:
能够自动检测出业务图片不合理使用: 比如解码的图片尺寸大于显示尺寸 2 倍以上等问题. 推动业务进行专项优化;
3)内存触顶监控:
能够检测出内存不足时占用内存较高的业务场景, 并定位到相应的页面, 推动业务进行优化.
以上这些技术都取得了很好的内存优化效果, 但他们的特点是: 主要针对明显的内存问题, 缺少更深入的内存分析.
因此, 手 Q 内存问题也一直存在, 主要表现在以下两方面:
1)手 Q 的平均内存一直持续增长, 版本间增幅较高, 手 Q 一月一个版本, 平均每版本增长大概 5.3M;
2)用户的 OOM 率大概 0.1%.
这次我们主要从监控和清理两个角度出发, 系统化的进一步优化手 Q 内存:
1)统一缓存监控: 开发实现全面的内存缓存监控系统, 能够更细致的监控手 Q 内存缓存使用情况, 及时发现轻度不合理问题, 推进优化;
2)内存清理 在监控的基础上, 开发实现自动清理机制: 一方面统一调度手 Q 各业务主动清理内存, 另一方面, 通过深入的技术研究, 实现系统内存清理技术.
通过监控和清理相互配合, 我们最终实现了优化手Q整体内存, 降低 OOM 率的效果. 以下是详细方案.
学习交流:
- 即时通讯开发交流群: 320837163 http://shang.qq.com/wpa/qunwpa?idkey=347e290d9cc726233b8c106272c100c8b56c366914452ebcd577f520e3617649 [推荐]
- 移动端 IM 开发入门文章:新手入门一篇就够: 从零开发移动端 IM http://www.52im.net/thread-464-1-1.html
腾讯技术分享: Android 手 Q 的线程死锁监控系统技术实践 http://www.52im.net/thread-1442-1-1.html
微信团队原创分享: iOS 版微信的内存监控系统技术实践 http://www.52im.net/thread-1422-1-1.html
微信团队原创分享: Android 内存泄漏监控和优化技巧总结 http://www.52im.net/thread-143-1-1.html
QQ 音乐团队分享: Android 中的图片压缩技术详解(上篇) http://www.52im.net/thread-1208-1-1.html
QQ 音乐团队分享: Android 中的图片压缩技术详解(下篇) http://www.52im.net/thread-1212-1-1.html
Android 版微信安装包 "减肥" 实战记录 http://www.52im.net/thread-138-1-1.html
iOS 版微信安装包 "减肥" 实战记录 http://www.52im.net/thread-137-1-1.html
微信客户端团队负责人技术访谈: 如何着手客户端性能监控和优化 http://www.52im.net/thread-921-1-1.html
3, 统一缓存监控
统一缓存监控主要包含图片缓存监控和业务对象缓存监控两部分:
1)图片缓存监控主要关注 Bitmap 的引用, 定位图片问题;
2)业务对象缓存监控, 主要监控手Q各业务对象缓存, 及时发现缓存问题.
3.1 图片缓存监控
对于 Android 应用来说, Bitmap 向来是内存的占用大户. 在手 Q 中平均有 300+ Bitmap 对象.
统计显示: Bitmap 引用内存占手 Q 总内存 40% 左右:
减少图片占用内存, 需要规范图片缓存的使用. 前期我们在手 Q 中封装实现了图片专用多级缓存 QQLruChe, 并要求各业务必须使用全局图片专用缓存来缓存图片. 一方面可以方便调控图片缓存业务, 另一方面, 通过淘汰以及清理策略, 可以有效控制图片缓存大小. 但由于手 Q 业务众多, 业务独立开辟图片缓存的情况还是时有发生. 因此我们开发了一套图片缓存监控系统, 及时检测出图片缓存私藏问题, 同时也监控图片的其他不合理使用.
图片缓存监控使用内存快照技术实现, 分为终端数据采集和后台数据分析两部分.
流程如下所示:
终端数据采集: 客户端实时检测当前可用内存, 当可用内存不足时, 自动生成内存快照文件, 上报到后台.
后台数据分析: 在后台, 我们实现了一套 Hprof 文件分析以及 Bitmap 引用归并技术, 批量分析内存快照文件, 输出 Bitmap 引用链并进行归类统计, 过滤全局图片专用缓存以及 View 层引用后, 分析出存在图片缓存私藏的业务.
实现图片缓存监控过程中我们主要遇到以下几个难点:
1)内存快照文件大, 约 300M 左右:
内存文件过大会导致上传流量和存储成本比较大, 而且上传耗时长. 针对这个问题, 一方面, 我们对大盘用户采样上报, 并提供良好的用户交互. 另一方面我们深入分析内存快照采集原理, 自研 miniDump 工具, 通过 native hook 技术在生成内存快照时剔除了 tyte[]数据, 从而使文件体积减少 70%;
2)内存快照文件人工分析成本高:
通过 MAT 人工分析内存快照文件耗时费力, 而且分析数量有限, 用户上报的内存文件很多, 无法定位 top 问题. 针对该问题, 我们深入研究 MAT 插件技术, 自研引用链分析以及 Bitmap 引用归并工具, 自动化分析内存快照文件, 归类 Bitmap 图片引用.
通过图片监控系统, 我们有效检测出以下几类业务问题:
1)全局图片专用缓存占用空间大, 存在优化空间:
bitmap 引用链归并发现全局图片专用缓存占较高. 同时, 我们也统计了 OOM 用户全局图片缓存的内存量, 平均约 10M 左右. 因此有必要在内存不足时, 自动 trimToSize, 释放内存空间;
2)业务 bug-- 逻辑完成后, 没有及时释放图片引用:
业务逻辑存在问题, 比如有几类业务在页面退出后, 没有及时释放背景图资源引用;
3)业务私自开辟图片缓存:
业务独立开辟缓存 cache 缓存 bitmap, 没有使用全局图片专用缓存;
4)业务缓存数据对象中引用图片:
业务内存缓存的数据对象中, 含有 bitmap 成员, 内存空间大. 可优化为缓存 key,bitmap 对象存到全局图片专用缓存中;
5)图片静态引用:
定义静态的 Bitmap 或者 Drawable 对象, 进程周期内, 对象所引用的资源都无法释放.
在手 Q730 版本, 图片缓存监控系统检测出 32 例业务问题, 提单 26 例, 累计节省内存约 23M.
3.2 业务对象缓存监控
业务对象缓存监控主要是通过实现自定义集合类, 实时上报各业务内存缓存使用情况到后台. 在后台分析归并, 从而定位业务缓存问题.
如上图所示, 业务对象缓存主要分为终端数据采集, 后台数据分析, 缓存清理三部分:
终端数据采集: 通过自定义实现 QQHashMap,QQConCurrentHashMap,QQLruCache 等集合类, 在系统原有集合类基础上, 封装统计功能, 实时统计程序运行期间各缓存的内存指标: 插入次数, 查询次数, 删除次数, 遍历次数, 命中次数, 未命中次数, 缓存使用率, 内存占用等;
后台分析: 分析终端上报的用户数据, 对各业务缓存进行归类统计, 统计出平均内存占用, 最大内存占用, 内存占用中位数, 缓存命中率, 缓存浪费率等指标;
内存清理: 监控系统在监控的基础上, 也增加了清理接口. 当检测到当前可用堆内存比较低, 用户处于内存高负荷状态时, 统一调度清理逻辑, 进行内存自清理优化.
通过统一缓存监控, 我们检测出了很多业务缓存问题, 主要可归为以下三类.
1)缓存浪费率高:
典型案例 1: 手 Q 表情某类缓存, 平均浪费率超过 88%, 相当于缓存 1000 个对象, 有 800 + 没有使用过;
典型案例 2: 某红包模板缓存, 存储后从不访问, 浪费率 100%.
针对这类问题, 推动业务优化内存缓存结构, 去除无用缓存, 优化缓存方案, 以降低浪费率.
2)缓存内存占用大:
典型案例 1: 手 Q 某新闻类图片缓存, 私自缓存 Bitmap, 最大占用内存 15M, 占所有图片缓存的 35%;
典型案例 2: 钱包类背景图缓存, 内存占用约 1M 左右, 使用后未及时释放.
针对这类问题, 对于缓存图片的业务, 推动业务接入全局图片专用缓存; 对于非图片类业务, 接入自动清理, 及时释放内存空间.
3)缓存结构存在优化空间:
典型案例 1: 讨论组成员缓存, 设计为 LRUCache 可淘汰缓存, 但是用户未曾用满过. 初始开辟空间过大;
典型案例 2: 未读消息缓存, 极端用户缓存个数超过 9000 个, 无数量上限控制.
针对这类问题, 推动业务更新或者优化缓存结构, 增加上限控制等.
4, 内存清理
统一监控, 可以有效发现业务缓存问题进行专项优化. 但监控具有一定的滞后性, 因此在监控的基础上, 我们同时也增加了内存清理控制模块.
内存清理主要分为业务内存清理以及系统内存清理. 业务内存清理, 包含统一图片缓存清理, 以及业务缓存对象清理两部分. 这里前文已简单介绍. 接下来我们介绍下两例系统相关的内存清理技术: 系统 ClassLoader 内存清理和系统预加载图片清理.
4.1 系统 ClassLoader 内存清理
前期, 我们分析了很多内存快照, 发现一个共性的问题: 在内存快照中有个 ZipFile 对象, 内存占用一直超过 2.6M. 这个 zipFile 被系统类 ClassLoader 引用.
通过分析系统源码, 我们发现 ZipFile 记录了安装包所有的类文件信息, 手 Q 安装包中有超过 15000 个文件, 文件越多, zipFile 占用内存就越大.
我们进一步分析 ClassLoader 相关源码, 发现只有在调用 ClassLoader 的 findResource 方法查找图片等安装包内资源时, 才会使用到 ZipFile 的内容, 未发现其他使用场景. 同时, 通过 findResource 方式查找资源存在一定的弊端: 耗时很长, 在 Android 系统上不推荐使用.
详情分析可参考:
http://blog.nimbledroid.com/2016/04/06/slow-ClassLoader.getResourceAsStream-zh.html
综合评估, 可以清理 ClassLoader 引用的这块内存.
清理主要面临以下几个难点:
1)Android 系统碎片化严重, 兼容性问题比较突出:
不同版本, zipFIle 成员变量的位置以及变量名不同. zipFile 初始化时机改变: 4.3 以前创建时即初始化, 4.3 之后, 第一次访问才会初始化. 各厂商对系统 API 内部修改无法预期;
2)强行清理, 可能导致功能异常:
系统内部代码逻辑可能会受到影响, 而且影响无法预期. 手 Q 当前使用 ClassLoader 查找资源的业务功能会受到影响. 后期新增业务无法预期, 清理会导致系统功能失效;
3)清理后再次加载 zipFile 耗时长, 可能导致卡顿.
下图是我们清理系统 ClassLoader 的实现方案, 采用代理, 兜底, 缓存, 上报等手段逐一攻克以上难点, 完美实现清理系统 ClassLoader 内存的效果.
1)针对兼容性问题, 我们通过反射代理替换了系统的 ZipFile 为 HookZipFile, 替换完成后, 清理掉 zipFile 内存. 替换机制兼容系统不同版本以及特殊机型, 对系统逻辑无影响.
2)针对清理导致的功能异常, 我们实现了兜底能力, 下次访问时, 会重新创建 zipFile.
3)针对耗时问题, 内部封装实现缓存功能. 并针对业务访问增加堆栈上报, 及时推动业务改用其他方式获取资源.
内存清理方案, 通过内部兼容性测试, 发布后外网无 crash 问题, 通过不断迭代, 兼容率达到 100%. 并且内存清理效果明显, 平均清理内存量约 2.6M.
4.2 系统预加载图片清理
系统预加载图片缓存是 zygote 进程初始化时, 通过 preloadResources()预加载的通用图片资源, 后续 android 应用进程都是从 Zygote fork 出来的, 所以就继承了这部分预加载的图片资源. 由于是静态强引用, 这部分图片资源会一直占用内存空间.
预加载的好处在于系统只在 zygote 执行一次加载操作, 所有应用用到该资源不需要再重新加载, 减少资源加载耗时. 与此同时, sPreloadedDrawables 属于静态对象, 会一直引用图片缓存, 所以该系统机制会占用较高内存, 在有些系统上, 内存占用空间超过 20M.
因此这里存在内存优化的空间, 当内存占用高时, 可以主动清理掉这部分内存, 以便释放可观的堆内存空间, 减少内存耗尽的风险. 通过分析 drawable 加载机制的源码, 我们了解到如果预加载的资源没有在 sPreloadedDrawables 中找到, 会重新 decode 解码加载, 不会影响现有功能.
因此清理后的风险可控, 主要面临的难点是兼容性问题:
1)系统 API 变动较多:
sPreloadDrawables 数据结构类型, 对象存储位置, 不同 API 版本之间都有改动;
2)厂商自定义修改较多:
比如: 小米 7.0 系统以及华为部分机型各自扩展了 ResourceImpl 实现, 自定义了自己的资源加载基类, 导致无法定位到 sPreloadDrawable;OPPO 部分机型, 修改了 sPreloadDrawable 类的属性等等.
下图是我们清理系统预加载图片缓存的实现方案, 通过反射替换的方式, 拦截替换系统的预加载缓存为自定义图片缓存, 内部管理图片加载, 在内存不足时, 及时清理预加载图片缓存.
针对兼容性问题, 我们实现了一套完备的兼容性方案:
1)替换前兼容检测;
2)系统版本兼容处理;
3)特定机型兼容处理;
4)失败上报统计, 不断兼容.
系统预加载图片清理, 通过不断迭代, 已经可以兼容几乎所有机型. 版本发布后, 未引入系统功能异常以及外网 crash 问题. 内存清理量比较可观: 平均在 15M 左右, 最高达到 25M.
5, 优化后的效果
5.1 横向对比
在灰度阶段, 我们对用户进行了 ABTest 测试, 一半用户接入内存优化逻辑, 一半用户不接入. 大盘上报统计显示: 优化用户 OOM 率明显低于未优化用户: OOM 率由 0.09% 降至 0.053%.
内存分布方面, 内存优化显著降低了高内存用户占比. 由 3.05% 降至 1.7%. 高内存用户是指当前可用内存不足 20% 的用户, 是 OOM 高发用户群, 降低这部分用户比例, 可有效降低 OOM 率.
5.2 纵向对比
我们从 7.3.0 版本接入内存优化, 从版本迭代来看, 优化效果显著: OOM 率由 0.09% 左右降低至 0.047% 左右, 降幅 47%:
手 Q 版本间平均内存增幅明显放缓, 版本增幅由 5.8M 左右降至 1.14M 左右:
来源: http://www.jianshu.com/p/4646b364ad17