安卓开发高手课—内存优化学习日志

Android Bitmap内存分配的变化

1 在Android3.0之前,Bitmap对象放在Java堆,而像素数据放在Native内存中,如果不手动调用recycle,Bitmap Native内存的回收完全依赖finalize函数回调,这个时机不太可控
2 Android3.0~Android7.0将Bitmap对象和像素数据统一放到Java堆中,这样就算我们不调用recycle,Bitmap内存也会随着对象一起被回收。不过Bitmap是内存消耗大户,把它放到Java堆中似乎不是那么美妙
3 Android8.0,将Bitmap内存放到Native中,也可以做到和对象一起快速释放,同时GC的时候会考虑这些内存,防止被滥用。通过NativeAllocationRegistry实现

Fresco图片库怎样在Android5.0~Android7.0将图片放到Native内存中

步骤一:通过调用libandroid_runtime.so中Bitmap的构造函数,可以得到一张空的Bitmap对象,而它的内存是放到Native堆中。但是不同Android版本的实现有那么一点差异,这里都需要适配

// 步骤一:申请一张空的 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;

这个方案的问题是兼容性和由于频繁申请和释放Java Bitmap导致的内存抖动

使用以下命令行代码查看某个应用的内存使用情况

adb shell dumpsys meminfo  [-d]

Java内存分配

Allocation Tracker和MAT

Allocation Tracker有以下几个缺点

1 获取的信息过于分散,中间夹杂着不少其他的信息,很多信息不是应用申请的可能需要进行不少查找才能定位到具体的问题
2 跟Traceview一样,无法做到自动化分析,每次都需要开发者手工开始/结束
3 虽然在Allocation Tracking的时候,不会对手机本身的运行造成过多的性能影响,但是在停止的时候,直到把数据dump出来之前,经常会把手机完全卡死,如果时间过长甚至会直接ANR

所以最好做到脱离Android Studio,实现一个自定义的“Allocation Tracker”,实现对象内存的自动化分析。但这个方案的兼容性问题会比较多,在Dalvik和ART中,Allocation Tracker的处理流程差异就非常大。

Native 内存分配

Android的Native内存分析一直做得非常不好,不过最近几个版本Google做了大量努力

在Android8.0之后可以根据这个(https://github.com/google/sanitizers/wiki/AddressSanitizerOnAndroidO)来使用AddressSanitizer
关于Native内存的问题,有两种方法,Malloc调试和Malloc钩子(http://source.android.com/devices/tech/debug/native-memory)

内存优化考虑哪些方面

1.设备分级

内存优化首先需要根据设备环境来综合考虑,并非“内存占用越少越好”,其实我们可以让高端设备使用更多的内存,做到针对设备性能好坏
使用不同的内存分配和回收策略
使用device-year-class的策略对设备分级,对低端设备可以关闭复杂动画,或者某些功能,使用565格式的图片,使用更小的缓存内存等

缓存管理

需要有一套统一的缓存管理机制,可以适当地使用内存,当内存吃紧时,及时释放内存,可以使用OnTrimMemory回调,根据不同的状态决定释放多少内存

进程模型

一个空的进程也会占用10MB的内存,而有些应用启动就有十几个进程,甚至有些应用已从双进程保活升级到四进程保活,所以减少应用启动的进程数、减少常驻进程、有节操的保活,对低端机内存优化非常重要

安装包大小

安装包中代码、资源、图片以及so库的体积,跟它们占用的内存有很大的关系。需要考虑针对低端机用户退出4mb的轻量版本,如今日头条极速版

2.Bitmap优化

Bitmap内存一般占应用总内存很大一部分,内存优化不能避开这个主题
即使将所有的Bitmap都放到Native内存,并不代表图片内存问题就完全解决了,这样做只是提升了系统内存利用率,减少了GC带来的一些问题而已
那到底怎样优化图片内存呢?

统一图片库

图片内存优化的前提是收拢图片的调用,这样我们就可以做整体的控制策略。

统一监控

大图片监控

开发过程中弹框提示图片所在的Activity和堆栈,在灰度和线上线下环境将异常信息上报到后台;

重复图片监控

重复图片指的是Bitmap像素数据完全一致,但是有多个不同的对象存在。自己实现内存Hprof分析工具图片总内存,通过收拢图片使用,可以统计应用所有图片占用的内存,这样在线上就可以按不同的系统、屏幕分辨率等维度去分析图片内存的占用情况。在OOM崩溃的时候,也可以把图片占用的总内存、TOP N 图片的内存都写到崩溃日志中,帮助排查问题

3.内存泄露

内存泄露分两种情况,一种是同一对象泄露,还有一种情况更加糟糕,就是每次都会泄露新的对象,可能会出现几百上千个无用的对象。

Java内存泄露

建立类似LeakCanary自动化检测方案,至少做到Activity和Fragment的泄漏检测。开发时,在出现内存泄露时弹框提醒。线上可以对生成的Hprof内存快照文件做一些优化,裁剪大部分图片对应的byte数组减少文件大小,然后使用7zip压缩,可以增加文件上传成功率

OOM监控

美团有一个Android内存泄露自动化链路分析组件Probe Android内存泄漏自动化
链路分析组件——Probe,它在发生OOM的时候生成Hprof内存快照,然后通过单独进程对这个文件做进一步分析。不过线上使用这个工具风险较大,有可能导致二次崩溃,耗时较长(几分钟),影响用户体验。另外部分OOM是因为虚拟内存不足导致,需要具体问题具体分析

Native内存泄露监控

之前讲到的Malloc调试和Malloc钩子似乎还不是那么稳定。在WeMobileDev最近的文章 微信 Android 终端内存优化实践,微信做了一些其他方案上面的尝试

针对无法重编的so

使用PLT Hook拦截库的内存分配函数,然后重定向到我们自己的实现后记录分配的内存地址、大小、来源so库路径等信息,定期扫描分配与释放是否配对,对于不配对的分配输出我们记录的信息

针对可重编的so

通过GCC的“-finstrument-functions”参数给所有函数插桩,桩中模拟调用栈入栈出操作;通过ld的“-wrap”参数拦截内存分配和释放函数,重定向到我们自己的实现后记录分配的内存地址、大小、来源so库路径以及插桩记录的调用栈此刻的内容,定期扫描分配与释放是否配对,对于不配对的分配输出我们记录的信息

目前,除了Java泄漏检测方案,OOM监控和Native内存泄露监控都只能做到实验室自动化测试的水平。微信的Native监控方案也遇到一些兼容性的问题,如果想达到灰度和线上部署,需要考虑的细节会非常多。

内存监控

1.采集方式

用户在前台时,可以每5分钟采集一次PSS、Java堆、图片总内存建议通过采样只统计部分用户,需要注意的是要按用户抽样,而不是按次抽样。简单来说一个用户如果命中采集,那么在一天内都要持续采集数据

2.计算指标

用户只上传数据,指标在后台计算,可计算下面一些内存指标
内存异常率:可以反映内存占用的异常情况,如果出现新的内存使用不当或内存泄露的场景,这个指标会有所上涨。
触顶率:可以反映Java内存的使用情况,如果超过85%最大堆限制,GC会变得更加频繁,容易造成OOM和卡顿。

3.GC监控

在实验室或内部试用环境,我们可以通过Debug.startAllocCounting来监控Java内存分配和GC的情况,对性能有影响,目前可以使用,但已经被标记为deprecated

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();

在Android6.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的次数和耗时,因为它会暂停应用线程,可能导致应用发生卡顿。

你可能感兴趣的:(安卓开发高手课—内存优化学习日志)