文章最后更新时间:2020.03.08html
性能优化是一个永不过期的话题,对于移动端来讲也是如此。即便如今的终端设备更新速度突飞猛进,但用户老是但愿你的应用能越快越好,秒启动,流畅无违和的操做,及时的操做响应等等。java
移动端性能优化是一个很泛的话题,以笔者目前的认知水平来讲,大概可分如下几个方面:android
对于通常的应用来讲,UI 渲染优化 和 内存优化 是两个必需要作的事情,由于这两个方面是平常开发比较常见的,同时对于用户来讲,也是更直接的影响。好比,若是内存出现问题,经常会致使长时间内存占用太高,继而致使内存抖动,更极端的状况会出现 OOM 异常,致使应用奔溃。若是 UI 绘制渲染出现问题,经常会出现丢帧的状况,对于用户来讲,就是卡顿,操做不流畅。git
关于 内存优化 这个话题,网上有不少相关的文章,会告诉你怎么去减小内存占用等等,因此这篇文章不会涉及这些知识,正像标题所说的同样,偏向于怎么去作内存监控。文章会分红上下两篇,这是第一篇,主要是调研现有的方案,包括微信开源的 Martix 中的 Matrix-Android-ResourceCanary,美团的 Probe:Android线上OOM问题定位组件,还有开源的 LeakCanary。第二篇,会尝试去开发一个内存监控组件。github
咱们先把相关技术难点先提出来,再带着问题去看看上面列举的这些方案的解决思路。shell
hprof 文件裁剪数组
hprof 是堆内存快照文件,能够方便的定位内存问题,但有一点不可忽视的问题,就是文件过大。hprof 文件动则 70M,80M 多,有的应用可能会超过 100M,对于测试环境来讲,这个可能不是问题,但若是在生产环境的话,对于用户设备来讲,这会是个不小的负担(不论是流量或者电量来讲),并且若是涉及日志回捞,那上传成功率可能会大打折扣。再者,当咱们在设备上进行分析操做时,内存的占用也会处于更高的水平线,这可能会加重应用当前的内存问题,甚至可能会致使 OOM。性能优化
实时监控微信
咱们须要监控哪些对象,好比 Activity,Fragment 或者是其余的对象,检查频率又该是怎么样的,好比一分钟检查一次,或者是一天检查一次等等。对于测试环境来讲,咱们能够采用更高的监控等级,监控更多的对象,使用更高的检查频率,生产环境则须要更为保守的方案。markdown
优化 dump 操做
Android 设备上,要 dump 当前内存快照,通常会调用 Debug.dumpHprofData()
方法,接入过 LeakCanary 的同窗会知道,当执行 dump 操做时,经常会致使应用停顿几秒,在生产环境执行 dump 操做的话,若是时间过长,可能会给用户带来糟糕的体验,甚至可能会触发 ANR。
可能还有其余问题,但暂时还没考虑到,后续再补充。
网上能搜集到方案可能不少,这里咱们选取几个比较有参考价值的:
技术更新很是快,后续有其余更好的方案,再继续更新。
LeakCanary 多是 Android 同窗很是熟悉的开源项目了,它有不少优势,如今已经开发了 2.0 了,使用新的 hrpof 分析工具 shark 代替原来的 haha,对于上面提出的难点,LeakCanary 主要作了 hprof 文件裁剪 和 实时监控。
LeakCanary 用 shark 组件来实现裁剪 hprof 文件功能,在 shark-cli 工具中,咱们能够经过添加 strip-hprof
选项来裁剪 hprof 文件,它的实现思路是:经过将全部基本类型数组替换为空数组(大小不变)。
执行如下命令:
shark-cli strip-hprofHPROF_FILE_PATH
复制代码
相关源码:
when (record) { is BooleanArrayDump -> BooleanArrayDump( record.id, record.stackTraceSerialNumber, BooleanArray(record.array.size) ) is CharArrayDump -> CharArrayDump( record.id, record.stackTraceSerialNumber, CharArray(record.array.size) { '?' } ) is FloatArrayDump -> FloatArrayDump( record.id, record.stackTraceSerialNumber, FloatArray(record.array.size) ) is DoubleArrayDump -> DoubleArrayDump( record.id, record.stackTraceSerialNumber, DoubleArray(record.array.size) ) is ByteArrayDump -> ByteArrayDump( record.id, record.stackTraceSerialNumber, ByteArray(record.array.size) ) is ShortArrayDump -> ShortArrayDump( record.id, record.stackTraceSerialNumber, ShortArray(record.array.size) ) is IntArrayDump -> IntArrayDump( record.id, record.stackTraceSerialNumber, IntArray(record.array.size) ) is LongArrayDump -> LongArrayDump( record.id, record.stackTraceSerialNumber, LongArray(record.array.size) ) else -> { record } } 复制代码
实验下来,这个裁剪的收益很小,72M 的文件大概只裁剪了 62KB,可能这个功能不是为了裁剪文件大小,而是其余目的,有知道的同窗能够告知下。
LeakCanary 主要是为测试环境开发,它会在 Activity 或者 Fragment 的 destory 生命周期后,能够检测 Activity 和 Fragment 是否被回收,来判断它们是否存在泄露的状况。更多相关知识,能够参考笔者以前的文章:LeakCanary2 源码分析。
微信开源的 Matrix 组件是个很是不错的项目,其中关于内存监控部分是 ResourceCanary,和 LeakCanary 同样, Matrix 主要也是在 hprof 文件裁剪 和 实时监控 这两方面作了一些优化。
Matrix 的裁剪思路主要是将除了部分字符串和 Bitmap 之外实例对象中的 buffer 数组。之因此保留 Bitmap 是由于 Matirx 有个检测重复 Bitmap 的功能,会对 Bitmap 的 buffer 数组作一次 MD5 操做来判断是否重复。
@Override public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) { final ID deduplicatedID = mBmpBufferIdToDeduplicatedIdMap.get(id); // Discard non-bitmap or duplicated bitmap buffer but keep reference key. if (deduplicatedID == null || !id.equals(deduplicatedID)) { if (!mStringValueIds.contains(id)) { skipData += elements.length; return; } } super.visitHeapDumpPrimitiveArray(tag, id, stackId, numElements, typeId, elements); } 复制代码
Martix 的裁剪收益仍是比较可观的,原文件 92M 裁剪后只有 18M。
Matrix 是基于 LeakCanary 上进行二次开发,因此监控原理基本是一致的,主要增长了一些误报的优化,好比:
屡次检测到相同的可疑对象,才认定为泄露对象,参数可配置。
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes || !mResourcePlugin.getConfig().getDetectDebugger()) { // Although the sentinel tell us the activity should have been recycled, // system may still ignore it, so try again until we reach max retry times. MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n" + "exists in %s times, wait for next detection to confirm.", destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount); continue; } 复制代码
增长一个哨兵对象,用于判断是否有 GC 操做,由于调用 Runtime.getRuntime().gc()
只是建议虚拟机进行 GC 操做,并不必定会进行。
if (sentinelRef.get() != null) { // System ignored our gc request, we will retry later. MatrixLog.d(TAG, "system ignore our gc request, wait for next detection."); return Status.RETRY; } 复制代码
避免重复检测相同的对象
if (sentinelRef.get() != null) { // System ignored our gc request, we will retry later. MatrixLog.d(TAG, "system ignore our gc request, wait for next detection."); return Status.RETRY; } 复制代码
Matrix 虽然是基于 LeakCnary,但额外增长了一些配置选项,能够用于生产环境,好比 dump 模式,支持手动触发 dump,自动 dump,和不进行 dump,能够根据不一样的环境,使用不一样的模式。
public enum DumpMode { NO_DUMP, AUTO_DUMP, MANUAL_DUMP, SILENCE_DUMP } 复制代码
除此以前,还有检测时间间隔等等。
还有一点就是,Matrix 是将分析 hprof 操做独立出来的,可使用 resource-canary-analyzer 去执行分析操做。
Probe 不是开源的,因此只能经过相关的文章 Probe:Android线上OOM问题定位组件 和一些其余手段去了解。
Probe 一样是在 hprof 文件裁剪 和 实时监控 这两方面作了优化。
注意事项⚠️:对 Probe 组件的研究纯粹只是出于学习目的,请不要用作其余用途。
与 Matrix 不一样的是,Probe 支持在设备上进行分析 hprof 操做,因此它不只考虑了 hprof 文件上传成功率,还考虑加载 hprof 带来的内存占用高问题。不论是 LeakCanary 仍是 Matrix,在裁剪的处理上,都是先将原始的 hprof 文件 dump 出来之后,过滤掉某些数据后,再从新写入到新的 hprof 文件。而 Probe 的处理则更加有创意,它经过 native hook 了虚拟机写入 hprof 文件的操做,在这里先过滤了某些数据,最终生成的 hprof 文件就是裁剪后的了。在不考虑可能会有的兼容性问题,这个方案要比其余的方案要高效,由于它解决了 hprof 加载的内存问题。
美团在测试中发现,分析 hprof 文件的占用内存和 hprof 中记录对象实例数量是成正比的,
测试时遇到的最大问题就是分析进程自身常常会发生OOM,致使分析失败。为了弄清楚分析进程为何会占用这么大内存,咱们作了两个对比实验:
- 在一个最大可用内存256MB的手机上,让一个成员变量申请特别大的一块内存200多MB,人造OOM,Dump内存,分析,内存快照文件达到250多MB,分析进程占用内存并不大,为70MB左右。
- 在一个最大可用内存256MB的手机上,添加200万个小对象(72字节),人造OOM,Dump内存,分析,内存快照文件达到250多MB,分析进程占用内存增加很快,在解析时就发生OOM了。
实验说明,分析进程占用内存与HPROF文件中的Instance数量是正相关的,在将HPROF文件映射到内存中解析时,若是Instance的数量太大,就会致使OOM。
计数压缩逻辑:若是存在重复的相同实例,则增长它的计数,不保存它的实例。Probe 定义超过实例超过 8000,则不会再继续保持它的实例对象。
除了文件裁剪,Probe 还优化分析泄露对象的链路,由于 Probe 相对于其余方案,理论上它是支持全部对象的内存泄露检测的(排除了原始类型等),而 LeakCanary 和 Matrix 只支持 Activity 和 Fragment 等对象,这个优化是基于 RetainSize 越大的对象对内存的影响也越大,是最有可能形成OOM的“元凶” 这一原则。
首先,在 dump 出全部对象后,建立一个 TOP N 的小根堆,根据 RetainSize 排序,初始 N 默认是 5,这里有个小细节,就是 Probe 会处理 ByteArray 类型的实例:
if (isByteArray(var6)) { var6.parent.addRetainedSize(var1.getHeapIndex(var4), var6.getTotalRetainedSize()); var15.add(var6.parent); } 复制代码
将 ByteArray 的 RetainSize 加到它的父节点,同时将它的父节点加到堆中。
在初始化小根堆后,继续遍历作动态调整,将大于文件大小 5% 的对象直接加到堆中,同时增大 TOP N 的值,不然,跟堆顶元素作比较。在遍历对象的同时,若是是相同类的不一样实例,则只会保存一份实例,同时将它们的 RetainSize 和 Num 计算进去:
if (var3.containsKey(var9)) { InstanceExtra var17 = (InstanceExtra)var3.get(var9); var17.retainSize += var6.getTotalRetainedSize(); ++var17.num; } 复制代码
还会将在 计数压缩逻辑 中 RetainSize 补回来:
if (var10 != null) { var16.retainSize += var10.getSize(); long var11 = var16.num; var16.num = (long)var10.getCount() + var11; ClassCountInfo var18 = (ClassCountInfo)AbandonedInstanceManager.getInstance().countMap.get(var9); if (var18 != null && var18.classId > 0L) { var16.id = var18.classId; } } 复制代码
Probe 对监控 Activity 对象的处理方法和 Matrix 的 DumpMode 相似,都是分级处理,Probe 线上监控到 Activity 泄露后,不会进行 dump 操做,只是对 Activity 类名等进行上报处理(这些都是能够配置的)。
除了 Activity 觉得,Probe 还会经过开启一个内存监控器,每隔 1s 查看当前应用可用内存,当剩余内存低于 10% (触底)时,会进行内存分析。
当发生 OOM 时,也会进行内存分析。
以前的配置都是经过接口下发的,这也能保证最大程度的灵活性。
从上面三种方案中,咱们能够得出一些结论:LeakCanary 虽然只会在测试环境使用,且只提供 Activity 和 Fragment 等对象的监控,但它提供监控的思路和 hprof 的分析,Matrix 和 Probe 或多或少都是基于它进行二次开发和优化。Matrix 主要是优化泄露对象的误判和 hprof 文件的裁剪,Probe 则是优化了在设备上进行 hprof 文件分析的内存占用,同时支持检测内存中全部对象(RetainSize 大的对象)。
惋惜的是,上面几种方案都没有涉及如何优化 heap dump 操做,这个留着后续研究。
Out of memory (OOM) 当系统没法知足申请的内存大小时,就会抛出 OOM 错误。致使 OOM 的缘由,除了咱们上面说讲的内存泄露之外,可能还会是线程建立超过限制,能够经过 /proc/sys/kernel/threads-max 获取:
cat /proc/sys/kernel/threads-max
26418
复制代码
还有一种多是 FD(File descriptor)文件描述符超过限制,能够经过 /proc/pid/limits 获取,其中 Max open files 就是可建立的文件描述符数量:
Limit Soft Limit Hard Limit Units
Max open files 4096 4096 files
复制代码