起因
最近做项目开发了一个通讯录模块,这个模块主要任务是处理和缓存联系人信息。因为在其他模块会用到这里的联系人数据,所以在创建和处理这些联系人数据的时候,为她加入了一些其他的属性信息。比如为了中文 T9 算法检索联系人,而使用 pinyin4j 生成的一些数据。所以这个联系人类还蛮大的。但前期架构设计中,考虑到其他模块检索联系人效率的问题,还是选择了空间换时间的做法。这也为后面查询内存泄露,导致界面卡顿埋下了伏笔。
是的,在开发后期功能点都完成后,开始对项目进行优化。在把能做的优化点都解决后,却发现通讯录模块,在多次切换 tab 后,会有卡顿现象,而且随着切换次数越多,卡顿现象越明显。这开始让我怀疑是不是通讯录这个模块,起初没有设计合理。反复的检查代码,虽然也找出了一些问题,但卡顿这么严重的问题依然会出现。
接下来就是坎坷的挖坑之路...
分析
先来欣赏一下,logcat 打印出来的内容。
只要滑动 RecyclerView 系统就会强制调用 GC 回收内存。一般情况下某个类在一个很短时间内申请大量内存才会报这个(其实这里是 Memory Churn 内存抖动的表现:大量的对象被创建又在短时间内马上被释放)。但经过检查代码逻辑,并没有频繁的申请大量内存啊?!通讯录的数据只会在第一次进入界面的时候加载,因为整个加载过程使用了三级缓存,且是使用 rxjava 的 concat() 方法来处理的。这又让我不得不怀疑,是缓存加载写的有问题,当然也有可能 rxjava 线程间切换出了问题。反复的检查,测试,打log。最终确定数据的缓存处理没有异常,数据只会加载并处理一次,操作数据是从缓存直接读取的。
使用 Monitors 来查看内存使用情况和 GPU 占用,也可以看出有一个操作大量占用内存和 GPU 资源
多次切换 tab 回到通讯录模块,快速滑动 RecyclerView 。内存的使用率会有一个明显的坡度,且在 GPU 的中可以看到蓝色线浮动很大,蓝色代表绘制操作。这样就可以确定一定是某个 view 过度绘制,甚至重复绘制造成资源消耗。
在 Monitors 的 Memory 中,点击 start allocation tracking 按钮,之后操作通讯录模块,让她内存升起来。再点一下 end allocation tracking 按钮。会有这样一条录制
同时会在编辑栏生成一个后缀为 .alloc 的文件
根据 total size 的大小来排序,发现 Thread1 在内存录制的过程中占用率高大98%,那问题一定出在这里了。一路点开 Thread1,找到 total size 最大的方法。
最终发现 RadiusImageView 就是那个罪魁祸首。果不其然将她替换成 Imageview 问题解决了。这里就不赘述 RadiusImageView 问题出在哪里了。
通过这次性能优化,也让我明白一个不经意的自定义 View 尽然会带来这么大的内存泄露问题。以后选择控件需要谨慎啊~
使用工具
在优化性能的时候,用到了很多工具和方法。这里简单的罗列一下。
手机系统的开发者选项:
- 开启布局界面 -- 可以帮助我们优化布局
- 开启调试 GPU 过度绘制 -- 可以清楚的帮助我们分析视图的 Overdraw
- 开启 GPU 呈现模式分析显示条形图 -- 可以帮助我们分析操作什么会大量使用 GPU 资源
当然实际上还有很多其他功能供我们使用,在这里就不赘述。接下来就是重要的辅助工具
有朋友说早在 Eclipse 时代,Android Device Monitor 就是我们重要的分析工具。在 Android Studio 中通过 tool -> Android -> Android Device Monitor 打开它。但我并不想再说这个工具,因为网上已经有很多它的使用分享。我想说的是 Android Studio 自带的 Monitors 工具。目前看来她是能解决我的基本需求的,而且她的使用也比之前的 Android Device Monitor 要友好很多。
总结
Android 性能优化是一个亘古不变的话题,项目到一定时期都会遇到,只是问题大小不同罢了。有一句很有名的话:过早的优化都是耍流氓。这句话没有问题,但我想说的是,虽然前期过多考虑优化和性能问题是过度工程的一种表现,但为了能合理的规避未知问题,我们应该在整个开发周期时刻关注性能,尽量不写出让工程 OOM 的代码。