内存产生的问题
- 异常
- 卡顿 频繁GC 物理内存不足触发
low memory killer
通过发送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 内存占用越少越好
系统内存足时多用一些以获得更好的性能;
内存不足时 希望用时分配 及时释放
关于Bitmap
内存分配的变化
- Android 3.0之前 Bitmap对象存在Java堆 像素数据存在Native内存中
不手动调用recycle
Bitmap Native内存的回收完全依赖finalize
时机不太可控 - Android 3.0~Android 7.0 Bitmap对象和像素数据统一放到Java堆中 不调用
recycle
Bitmap内存也会是随对象一起被回收
不过Bitmap消耗内存太大 还是不合适 - Android 8.0 利用
NativeAllocationRegistry
实现像素数据存放到Native中 并且新增了硬件位图Hardware Bitmap
可以减少图片内存并且提升绘制效率
NativeAllocationRegistry:
一种实现 可以将Bitmap
内存放到Native
中 也可以做到和对象一起快速释放 同时GC的时候也能考虑到这些内存防止被滥用
误区二: Native内存不用管
将图片的内存放到 Native
中
// 步骤一:申请一张空的 Native Bitmap
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;
测量方法
adb shell dumpsys meminfo <package_name|pid> [-d]
Java 内存分配
自定义 Allocation Tracker
需要考虑很多兼容性问题 在 Dalvik
和 ART
中 Allocation Tracker
的处理流程差异较大
Native 内存分配
Android 8.0 之后使用 AddressSanitize
检测内存
Native内存问题的两种方法:
- Malloc 调试
可以帮助我们调试Native
内存的问题 例如 堆破坏 内存泄漏 非法地址
Android 8.0 之后支持在非root的设备做Native内存调试 和AddressSanitize
一样 需要通过wrap.sh
做包装
adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
- Malloc 钩子
在Android P 之后 Android 的libc 支持拦截在程序执行期间所有 分配/释放 调用 这样我们可以构建出自定义的内存检测工具
adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'
代码 图片 资源以及so库和内存的关系.png
怎么优化?
Bitmap优化
- 统一图片库
图片内存优化的前提是收拢图片的调用 这样可以做整体的控制策略 - 统一监控
1⃣️ 大图监控
我们需要注意某张图片内存占用是否过大 例如长宽远远大于View
开发过程中 检测到不合规的图片使用 应该立即弹出对话框提示图片所在的Activity和堆栈
2⃣️重复图片监控
重复图片指的是Bitmap
的像素数据完全一致 但是有多个不同的对象存在
内存泄漏
简单的说就是没有回收不再使用的内存
内存泄漏分为两种情况:1⃣️ 同一个对象泄漏 2⃣️ 泄漏新的对象
- Java内存泄漏
建立类似LeakCanary
自动化检测方案 至少做到Activity
和Fragment
的泄漏检测
内存泄漏线上优化: 对生成的Hprof
内存快照文件做一些优化 裁剪大部分图片对应的byte数组减少文件大小 - OOM监控
美团的组件:Probe
在发生OOM的时候生成Hprof
内存快照 然后通过单独进程对这个文件做进一步分析 但是线上使用风险较大 - Native 内存泄漏监控
Malloc
但不是很稳定 - 针对无法重编so的情况
使用了PLT Hook
(Native Hook 的一种方案) 拦截库的内存分配函数 然后重定向到我们自己到实现后记录分配到内存地址 大小 来源so库路径等信息 定期扫描分配和释放是否配对 对于不配对等分配输出我们记录的信息 - 针对可重编的so
通过GCC的-finstrument-functions
参数给所有函数插桩 桩中模拟入栈出栈操作;
通过Id的-wrap
参数拦截内存分配和释放函数 重定向到我们自己到实现后记录分配到内存地址 大小 来源so以及插桩记录的调用栈的内容 定期扫描分配和释放是否配对 不配对的输出记录的信息
开发过程中 内存泄漏排查可以使用 Androd Profiler
和MAT
工具配合使用
内存监控
采集方式
用户在前台的时候 可以每5分钟采集一次PSS
(比例分配共享库占用的内存 ) Java堆 图片总内存
建议只统计部分用户
计算指标
内存异常率 可以反映内存占用的异常情况
PSS
的值可以通过 Debug.MemoryInfo
获取
内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV
UV 指独立访客
触顶率 可以反映Java内存的使用情况 如果超过85%最大堆限制 GC会变的更加频繁 造成OOM和卡顿
内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
是否触顶可以通过下面的方法计算得到:
内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
一般客户端只上报数据 所有计算放在后台
GC监控
测试环境 可以通过 Debug.startAllocCounting
来监控Java内存分配和GC
对性能有影响 已经被标记为 deprecated
可以拿到内存分配的次数 大小 以及GC发起的次数
long allocCount = Debug.getGlobalAllocCount(); //内存分配次数
long allocSize = Debug.getGlobalAllocSize(); //内存分配大小
long gcCount = Debug.getGlobalGcInvocationCount(); //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的次数和耗时 它会暂停应用线程 可能导致应用发生卡顿
网友评论