一. 内存问题
1.概念:
VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

- 内存问题会导致两个结果,一个是异常,如OOM,内存分配失败等。
第二个问题是卡顿。Java内存不足会导致频繁GC。
测试GC性能可以通过SIGQUIT 信号获得 ANR 日志。
adb shell kill -S QUIT PID
adb pull /data/anr/traces.txt
它包含一些 ANR 转储信息以及 GC 的详细性能信息
sticky concurrent mark sweep paused: Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms // GC 暂停时间
Total time spent in GC: 502.251ms // GC 总耗时
Mean GC size throughput: 92MB/s // GC 吞吐量
Mean GC object throughput: 1.54702e+06 objects/s
- 两个误区:
误区1:内存占用越少越好
应用是否占用了过多内存跟设备、系统和当时环境有关,不是300MB这样的绝对数值。要做到的是“用时分配,及时释放”
Bitmap内存优化
Android3.0之前Bitmap对象存放在Java堆,像素数据在Native内存中,如果不手动调用recyle,native内存的回收时机不可控。
Android3.0~7.0将Bitmap对象和像素数据统一存放到Java堆中,这样就算我们不调用recycle,Bitmap内存也会随着对象一起被回收。由于Java堆内存有限制,所以也可能会导致OOM,并且容易引起大量GC。
Android8.0采用NativeAllocationRegistry。一种实现 可以将 Bitmap内存放到 Native中 也可以做到和对象一起快速释放 同时GC的时候也能考虑到这些内存防止被滥用
详细了解可以参考这篇文章Android Bitmap变迁与原理解析(4.x-8.x)
误区二:Native 内存不用管
当系统物理内存不足额时候,lmk剋是杀进程,甚至可能导致手机重启。
Fresco 图片库在 Dalvik 会把图片放到 Native内存上。在Android5.0~7.0上也能做到,只是流程相对复杂。
// 步骤一:申请一张空的 Native Bitmap.直接调用 libandroid_runtime.so 中 Bitmap的
//构造函数饿到内存在native的Bitmap对象。到那时Android版本的实现差异需要适配
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);
// 步骤二:申请一张普通的 Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);
// 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);
// 步骤四:释放 Java Bitmap 内存
srcBitmap.recycle();
srcBitmap = null;
- 查看内存使用情况
可以参考《调查 RAM 使用情况》
adb shell dumpsys meminfo <package_name|pid> [-d]
工具:Allocation Tracker
不过他有三个缺点:https://mp.weixin.qq.com/s/b_lFfL1mDrNVKj_VAcA2ZA?
- 获取的信息过于分散,中间夹杂着不少其他的信息,不完全是app申请的,可能需要进行不少查找才能定位到具体的问题
- 跟TraceView一样,无法做到自动化分析,每次都需要开发者手工开始/结束,对于某些问题的分析可能会造成不便,而且对于批量分析来说也比较困难
- 虽然在allocation tracking的时候,不会对手机本身的运行造成过多的性能影响,然而在停止allocation tracker的时候,直到把数据dump出来之前,会把手机完全卡死,时间过长甚至会直接ANR
- 自定义的“Allocation Tracker”
要考虑的兼容性问题比较多。下面是在 Dalvik 和 ART 中,Allocation Tacker 的开启方式。
// dalvik
bool dvmEnableAllocTracker()
// art
void setAllocTrackingEnabled()
https://github.com/AndroidAdvanceWithGeektime/Chapter03
- Android8.0
AddressSanitizer
指南
调试本机内存使用Malloc 调试和Malloc 钩子。
二. 优化
1. 设备分级
使用类似device-year-class的策略堆设备进行分级,对低端机关闭复杂动画,使用小内存缓存等
if (year >= 2013) {
// Do advanced animation
} else if (year >= 2010) {
// Do simple animation
} else {
// Phone too slow, don't do any animations
}
2. 缓存管理
尽量用统一的一套缓存机制,在内存不足的时候,如OnTrimMemory 回调,根据不同的状态释放一部分内存。
3. 进程模型
一个空进程也会赵勇10MB内存,减少进程数 。
4. 安装包大小
安装包中的代码、资源、图片以及so库的体积,跟占用的内存有很大的关系。参考表格

5. Bitmap优化
- 统一图片库
收拢图片的调用入口,方便做整体控制。低端机可以i使用565格式,更加严格的缩 放算法,可以使用Glide、Fresco或者自研,收拢Bitmap.creatBitmap、BitmapFactory相关接口。 - 统一监控
<1>大图监控,例如长款远远大于View甚至是屏幕的长宽。
<2>重复图片监控。重负图片是指Bitmap的像素数据完全一样,但是有多个不同对象存在。通过分析内存文件hprof快速判断内存中是否存在重复的图片,并且将这些重复图片的PNG、堆栈等信息输出。参考https://github.com/AerialLadder/Chapter04
6. 简单的说就是没有回收不再使用的内存
内存泄漏分为两种情况: 同一个对象泄漏 、泄漏新的对象
- 建立类似 LeakCanary 自动化检测方案 至少做到Activity和Fragment的泄漏检测。
内存泄漏线上优化: 对生成的Hprof 内存快照文件做一些优化 裁剪大部分图片对应的byte数组减少文件大小 - OOM监控
美团的Probe 在发生OOM的时候生成Hprof内存快照,然后通过单独进程对这个文件做进一步分析。但是线上使用风险较大。 - Native 内存泄漏监控。
可以参考微信 Android 终端内存优化实践 - 针对无法重编so的情况
使用了 PLT Hook(Native Hook 的一种方案) 拦截库的内存分配函数 然后重定向到我们自己到实现后记录分配到内存地址 大小 来源so库路径等信息 定期扫描分配和释放是否配对 对于不配对等分配输出我们记录的信息 - 针对可重编的so
通过GCC的-finstrument-functions参数给所有函数插桩 桩中模拟入栈出栈操作;
通过Id的-wrap 参数拦截内存分配和释放函数 重定向到我们自己到实现后记录分配到内存地址 大小 来源so以及插桩记录的调用栈的内容 定期扫描分配和释放是否配对 不配对的输出记录的信息。
7. 内存监控
- 采集方式
用户在前台的时候 可以每5分钟采集一次PSS(比例分配共享库占用的内存 ) Java堆 图片总内存
建议只统计部分用户 - 计算指标
内存异常率 可以反映内存占用的异常情况
PSS 的值可以通过 Debug.MemoryInfo 获取
内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
UV 指独立访客
触顶率 可以反映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监控
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的次数和耗时 它会暂停应用线程 可能导致应用发生卡顿
网友评论