参考文章:https://time.geekbang.org/column/article/71610
定好优化目标:
比如针对 512MB 的设备和针对 2GB 以上的设备,完全是两种不同的优化思路。
面向东南亚、非洲用户,那对内存优化的标准就要变得更苛刻一些。
内存优化 3 方面
设备分级、Bitmap 优化和内存泄漏这三个方面入手。
1、设备分级
- 类似 device-year-class ,低端机关闭复杂动画,或者某些功能,使用 565 格式图片,更小缓存等。
if (year >= 2013) {
// Do advanced animation
} else if (year >= 2010) {
// Do simple animation
} else {
// Phone too slow, don't do any animations
}
- 缓存管理
统一缓存。当 系统内存不足 时,及时释放内存。 - 进程模型
一个 空进程也会占用 10MB 内存。有节操的增加进程。 - 安装包大小
代码、资源、图片以及 so 库的体积,跟它们占用的内存有很大的关系。
2、Bitmap 优化
Android bitmap 演进分析:
-
Android 2.x系统,当dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值时候就会发生OOM。其中bitmap是放于external中 。
Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,熟悉 Java 的同学应该知道,这个时机不太可控。
Android 2.x 系统 BitmapFactory.Options 里面隐藏的的inNativeAlloc反射打开后,申请的bitmap就不会算在external中。 -
Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中。
这样就算我们不调用 recycle,Bitmap 内存也会随着对象一起被回收。
Java 堆限制也才到 512MB,可能我的物理内存还有 5GB,但是应用还是会因为 Java 堆内存不足导致 OOM.
Android 4.x系统,废除了external的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= dalvik heap 最大值的时候就会发生OOM(art运行环境的统计规则还是和dalvik保持一致)
可采用facebook的fresco库,即可把图片资源放于native中。 -
android 8.x 从Java heap 移到了native heap
有没有一种实现,可以将 Bitmap 内存放到 Native中,也可以做到和对象一起快速释放,同时 GC 的时候也能考虑这些内存防止被滥用?NativeAllocationRegistry 可以一次满足你这三个要求,Android 8.0 正是使用这个辅助回 Native 内存的机制.
Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率。
具体方法:
-
统一图片库
eg:低端机使用 565 格式。可以使用 Glide、Fresco 或者采取自研都可以。而且需要进一步将所 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。 -
统一监控
在统一图片库后就非常容易监控 Bitmap 的使用情况了,这里主要有三点需要注意。
- 大图片监控。 防止超宽。像素浪费
即图片的大小不应该超过view的大小。对此,我们可以重载drawable与ImageView.
图片存在像素浪费,合理利用.9图 - 重复图片监控。
作者回复:这个重复bitmap分析是在服务器后台做的,目前是对所有 bitmap 数组直接计算hash的方法匹对。 - 图片总内存。
在 OOM 崩溃的时候,也可以把图片占用的总内存、Top N 图片的内存都写到崩溃日志中,帮助我们排查问题。
一个好的imageLoader,可以将2.X、4.X或5.X对图片加载的处理对使用者隐藏,同时也可以将自适应大小、质量等放于框架中。
3、内存泄漏
内存泄漏简单来说就是没有回收不再使用的内存。
内存泄漏主要分两种:
同一个对象泄漏。
每次都会泄漏新的对象,可能会出现几百上千个无用的对象。
优秀的框架设计可以减少甚至避免程序员犯错。很多内存泄漏都是框架设计不合理所导致,各种各样的单例满天飞,MVC 中 Controller 的生命周期远远大于 View。
- Java 内存泄漏
建立类似 LeakCanary 自动化监测方案。
在开发过程,我们希望出现泄漏时可以弹出对话框,让开发者更加容易去发现和解决问题。 - OOM 监控
美团有一个 Android 内存泄露自动化链路分析组件 Probe ,它在发生 OOM 的时候生成 Hprof 内存快照,然后通过单独进程对这个文件做进一步的分析。不过在线上使用这个文件做进一步的分析。不过在线上使用这个工具风险还是比较大,在崩溃的时候生成内存快照有可能会导致二次崩溃,而且部分手机生成 Hprof 快照可能会耗时几分钟,这对用户造成的体验影响比较大。 - Native 内存泄漏监控
上一期我讲到 Malloc 调试(Malloc Debug)和 Malloc 钩子(Malloc Hook)似乎还不是那么稳定。在 WeMobileDev 最近的一篇文章《微信 Android 终端内存优化实践》中,微信也做了一些其他方案上面的尝试。https://mp.weixin.qq.com/s/KtGfi5th-4YHOZsEmTOsjg?
目前还不那么完善。
开发过程中内存泄漏排查可以使用 Androd Profiler 和 MAT 工具配合使用,而日常监控关键是成体系化,做到及时发现问题。
内存监控
内存泄漏的监控存在一些性能的问题,一般只会对内部人员和极少部分用户开启。
线上:需要通过其他更有效的方式去监控内存相关的问题。
-
采集方式
要按照用户抽样,而不是按次抽样。持续采集命中用户。
用户在前台的时候,可以每 5 分钟采集一次 PSS、Java 堆、图片总内存。 -
计算指标
内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
其中 PSS 的值可以通过 Debug.MemoryInfo 拿到 其中 PSS 的值可以通过 Debug.MemoryInfo 拿到。
触顶率:可以反映 Java 内存的使用情况,如果超过 85% 最大堆限制,GC 会变得更加频繁,容易造成 OOM 和卡顿。
内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
long javaMax = runtime.maxMemory();
long javaTotal = runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
// Java 内存使用超过最大限制的 85%
float proportion = (float) javaUsed / javaMax;
一般客户端只上报数据,所有计算都在后台处理,这样可以做到灵活多变。
- GC监控
在实验室或者内部试用环境,我们也可以通过 Debug.startAllocCounting 来监控 Java 内存分配和 GC 的情况,需要注意的是这个选项对性能有一定的影响,虽然目前还可以使用,但已经被 Android 标记为 deprecated。通过监控,我们可以拿到内存分配的次数和大小,以及 GC 发起次数等信息。
long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
在 Android 6.0 之后系统可以拿到更加精准的 GC信息。
// 运行的 GC 次数
Debug.getRuntimeStat("art.gc.gc-count");
// GC 使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式 GC 的次数
Debug.getRuntimeStat("art.gc.blocking-gc-count");
// 阻塞式 GC 的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");
需要特别注意阻塞式 GC 的次数和耗时,因为它会暂停应用线程,可能导致应用发生卡顿。