解决 SVGAPlayer 内存问题的复盘总结
本文是基于真实项目优化而来,具体代码已经贡献给组织。
SVGAPlayer-Android 这个库做的还是很实用的,但是存在着很严重的内存问题。为了提升项目的质量和稳定性,对其进行一些修改,并提交给组织。
修复工作整体分为两步:
- 修复内存泄漏问题。
- 修复运行时无用资源对内存的消耗。
修复内存泄漏问题
发现内存泄漏的问题过程比较简单,重点是如何发现。在这次解决问题主要步骤就是使用 MMAT 工具分析内存泄漏问题,然后定位分析,解决问题。
总的来说内存泄漏的都是由于长生命周期的对象引用了短生命的对象导致短生命周期的对象无法被 GC 算法识别并回收。
在日常开发中,内存泄漏问题常常出现在以下场景中例如:Handler 强引用 Context、单利强引用 Context、匿名内部类强引用外部类(动画的回调、各种 Listener 等)。
解决的办法也属于模板化:将强引用转化为弱引用、在合适时机将强引用设置为 null 或者清理引用。
解决内存释放问题
在解决了项目内部和 SVGAPlayer 的内存泄漏后项目的稳定性并没有明显提升,根据 firebase 的统计头部问题都是和 OOM 有关。问题主要体现在 libc.so 和 Marshallable.java
Marshallable.java 是声网 SDK 内部的类,经过分析崩溃的具体地址为 ByteBuffer 手动开辟内存的时候造成的,对此和声网做了一些问题反馈
在反馈问题后,总是觉得怪怪的。直觉告诉我真正的问题并不一定是在声网内部的,这里只不过是压死骆驼的最后一根稻草。使用 Android Studio 的 Profile 工具简单分析后发现在聊天室运行时大量的内存都被 SVGAVideoEntity 消耗了,而 SVGAVideoEntity 内部持有大量的 Bitamp。由此怀疑内存泄漏的主要来源是 SVGAPlayer。
在 dump 内存并使用 MAT 工具分析后验证了了猜想:
在第二张图片中定位到了OOM的根本原因:mCarImageView(SVGAImageView) 内部强引用了一个 SVGADrawable -> SVGAVideoEntity -> HashMap -> 好多 Bitmap。简单的说就是 SVGA 在播放完毕后没有消除对 SVGADrawable 的引用。这也证明了 issue 关于内存部分的设计缺陷 的说法对的。
在明确问题之后,初步的解决方案是将 SVGAVideoEntity 内部所有的集合都改为弱引用
试验1:将强引用转换为弱引用 or 软引用
经过试验证明这是不可取的,如果对象只有弱引用存在,那么其被 GC 算法扫描到就会立即回收。这会导致内部的Bitmap 等对象还没有被 SVGAImageView 绘制就会被回收。
弱引用的方案不可取直接换成软引用行不行呢?事实证明还是不行。SVGAVideoEntity 对象体积实在是太大了(内部有很多 Bitmap)一旦对象被创建,很容易引发内存不足的问题,一样会被 GC 掉。
事实证明这个方案不符合此场景不可取。
试验2:在绘制完毕后 or 合适的时机将 SVGAImageView#Drawalbe 设置为null
这个方案的思路是间接切断 SVGAImageView 与 Bitmap 的引用关系,让其可被 GC。此方案重点关注的点有两个:
- 不能清理循环播放的动画
- 找到清理的合适时机。
试验3:传递 frame size 计算 Bitmap 采样率减少内存消耗
首先重构了代码,但是这个步子走的太大了,浪费了很多时间,而且在出了问题后因为无法定位问题,不得不回退了所有代码。
改变了软件的流程(错误的)一开始计算 Bitmap 采样率的逻辑改动了 SVGVideoEntity 的解析流程,将SVGVideoEntity 构造函数内解析 Bitamp 等 frame 信息推移动了绘制的 SVGAImageView 绘制的时候,这会导致加载变慢,而且改变了软件的原始流程。这个问题在何老师的指出后做了修改。要牢记解决问题是目标,但不能因此无所不用其极,这样不仅看起来怪怪的,而且还会出现问题。
试验4:暴露 Bitmap 创建接口,让使用者自定义 Bitmap 创建实现
经过尝试之后发现 Bitmap 的生命周期外移,会因 Bitmap 回收导致崩溃的问题,这个问题不好解决,已经移除了。
走的弯路
-
重构步子不能太大。SVGAPlayer 的代码质量并不高,一开始我就着手代码的重构,这浪费了很多时间去测试、解决重构引发的 Bug。在出现问题没法解决后,只能还原所有的代码。
-
解决问题才是根本目的,不能改变软件的原始流程。
-
不要看什么都不顺眼,要量力而行。
-
加深学习并发编程相关的知识。
经验总结
-
使用抽象类 + 泛型 + 模板方法可以减少代码的重复
-
命名要结合场景,看看老代码的命名方式,取一些符合项目场景的名字,让使用者一目了然
-
步子一定要小,对于重构老代码,一定要走一步测试一步,不然一旦出了问题,都不知道哪里改错了。不仅排查起来费劲,而且还可能无法发现问题回退代码。
-
不要看什么都不顺眼,目的是解决问题。先要解决问题,再去搞其他的东西,不然步子就乱了,影响进度。
-
书要看,但是要多写代码去实践。