深刻探索 Android 内存优化(炼狱级别)

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

本篇是 Android 内存优化的进阶篇,难度能够说达到了炼狱级别,建议对内存优化不是很是熟悉的仔细看看前篇文章: Android性能优化以内存优化,其中详细分析了如下几大模块:php

  • 1)、Android的内存管理机制
  • 2)、优化内存的意义
  • 3)、避免内存泄漏
  • 4)、优化内存空间
  • 5)、图片管理模块的设计与实现

若是你对以上基础内容都比较了解了,那么咱们便开始 Android 内存优化的探索之旅吧。html

本篇文章很是长,建议收藏后慢慢享用~前端

目录

  • 1、重识内存优化
    • 一、手机RAM
    • 二、内存优化的纬度
    • 三、内存问题
  • 2、常见工具选择
    • 一、Memory Profiler
    • 二、Memory Analyzer
    • 三、LeakCanary
  • 3、Android内存管理机制回顾
    • 一、Java 内存分配
    • 二、Java 内存回收算法
    • 三、Android 内存管理机制
    • 四、小结
  • 4、内存抖动
    • 一、那么,为何内存抖动会致使 OOM?
    • 二、内存抖动解决实战
    • 三、内存抖动常见案例
  • 5、内存优化体系化搭建
    • 一、MAT回顾
    • 二、搭建体系化的图片优化 / 监控机制
    • 三、创建线上应用内存监控体系
    • 四、创建全局的线程监控组件
    • 五、GC 监控组件搭建
    • 六、创建线上 OOM 监控组件:Probe
    • 七、实现 单机版 的 Profile Memory 自动化内存分析
    • 八、搭建线下 Native 内存泄漏监控体系
    • 九、设置内存兜底策略
    • 十、更深刻的内存优化策略
  • 6、内存优化演进
    • 一、自动化测试阶段
    • 二、LeakCanary
    • 三、使用基于 LeakCannary 的改进版 ResourceCanary
  • 7、内存优化工具
    • 一、top
    • 二、dumpsys meminfo
    • 三、LeakInspector
    • 四、JHat
    • 五、ART GC Log
    • 六、Chrome Devtool
  • 8、内存问题总结
    • 一、内类是有危险的编码方式
    • 二、普通 Hanlder 内部类的问题
    • 三、登陆界面的内存问题
    • 四、使用系统服务时产生的内存问题
    • 五、把 WebView 类型的泄漏装进垃圾桶进程
    • 六、在适当的时候对组件进行注销
    • 七、Handler / FrameLayout 的 postDelyed 方法触发的内存问题
    • 八、图片放错资源目录也会有内存问题
    • 九、列表 item 被回收时注意释放图片的引用
    • 十、使用 ViewStub 进行占位
    • 十一、注意定时清理 App 过期的埋点数据
    • 十二、针对匿名内部类 Runnable 形成内存泄漏的处理
  • 9、内存优化常见问题
    • 一、大家内存优化项目的过程是怎么作的?
    • 二、你作了内存优化最大的感觉是什么?
    • 三、如何检测全部不合理的地方?
  • 10、总结
    • 一、优化大方向
    • 二、优化细节
    • 三、内存优化体系化建设总结

1、重识内存优化

Android给每一个应用进程分配的内存都是很是有限的,那么,为何不能把图片下载下来都放到磁盘中呢?那是由于放在 内存 中,展现会更 “”,快的缘由有两点,以下所示:java

  • 1)、硬件快:内存自己读取、存入速度快。
  • 2)、复用快:解码成果有效保存,复用时,直接使用解码后对象,而不是再作一次图像解码。

这里说一下解码的概念。Android系统要在屏幕上展现图片的时候只认 “像素缓冲”,而这也是大多数操做系统的特征。而咱们 常见的jpg,png等图片格式,都是把 “像素缓冲” 使用不一样的手段压缩后的结果,因此这些格式的图片,要在设备上 展现,就 必须通过一次解码,它的 执行速度会受图片压缩比、尺寸等因素影响。(官方建议:把从内存中淘汰的图片,下降压缩比后存储到本地,以备后用,这样能够最大限度地下降之后复用时的解码开销。)linux

下面,咱们来了解一下内存优化的一些重要概念。android

一、手机RAM

手机不使用 PCDDR内存,采用的是 LPDDR RAM,即 ”低功耗双倍数据速率内存“。其计算规则以下所示:git

LPDDR系列的带宽 = 时钟频率 ✖️内存总线位数 / 8
LPDDR4 = 1600MHZ ✖️64 / 8 ✖️双倍速率 = 25.6GB/s。
复制代码

那么内存占用是否越少越好?

当系统 内存充足 的时候,咱们能够 多用 一些得到 更好的性能。当系统 内存不足 的时候,咱们但愿能够作到 ”用时分配,及时释放“。github

二、内存优化的纬度

对于Android内存优化来讲又能够细分为以下两个维度,以下所示:web

  • 1)、RAM优化
  • 2)、ROM优化

一、RAM优化

主要是 下降运行时内存。它的 目的 有以下三个:算法

  • 1)、防止应用发生OOM
  • 2)、下降应用因为内存过大被LMK机制杀死的几率
  • 3)、避免不合理使用内存致使GC次数增多,从而致使应用发生卡顿

二、ROM优化

下降应用占ROM的体积,进行APK瘦身。它的 目的 主要是为了 下降应用占用空间,避免因ROM空间不足致使程序没法安装

三、内存问题

那么,内存问题主要是有哪几类呢?内存问题一般来讲,能够细分为以下 三类:

  • 1)、内存抖动
  • 2)、内存泄漏
  • 3)、内存溢出

下面,咱们来了解下它们。

一、内存抖动

内存波动图形呈 锯齿张GC致使卡顿

这个问题在 Dalvik虚拟机 上会 更加明显,而 ART虚拟机内存管理跟回收策略 上都作了 大量优化内存分配和GC效率相比提高了5~10倍,因此 出现内存抖动的几率会小不少

二、内存泄漏

Android系统虚拟机的垃圾回收是经过虚拟机GC机制来实现的。GC会选择一些还存活的对象做为内存遍历的根节点GC Roots,经过对GC Roots的可达性来判断是否须要回收。内存泄漏就是 在当前应用周期内再也不使用的对象被GC Roots引用,致使不能回收,使实际可以使用内存变小。简言之,就是 对象被持有致使没法释放或不能按照对象正常的生命周期进行释放。通常来讲,可用内存减小、频繁GC,容易致使内存泄漏

三、内存溢出

即OOM,OOM时会致使程序异常。Android设备出厂之后,java虚拟机对单个应用的最大内存分配就肯定下来了,超出这个值就会OOM。单个应用可用的最大内存对应于 /system/build.prop 文件中的 dalvik.vm.heapgrowthlimit

此外,除了因内存泄漏累积到必定程度致使OOM的状况之外,也有一次性申请不少内存,好比说 一次建立大的数组或者是载入大的文件如图片的时候会致使OOM。并且,实际状况下 不少OOM就是因图片处理不当 而产生的。

2、常见工具选择

Android性能优化以内存优化 中咱们已经介绍过了相关的优化工具,这里再简单回顾一下。

一、Memory Profiler

做用

  • 1)、实时图表展现应用内存使用量
  • 2)、用于识别内存泄漏、抖动等
  • 3)、提供捕获堆转储、强制GC以及根据内存分配的能力

优势

  • 1)、方便直观
  • 2)、线下使用

二、Memory Analyzer

强大的 Java Heap 分析工具,查找 内存泄漏及内存占用, 生成 总体报告分析内存问题 等等。建议 线下深刻使用

三、LeakCanary

自动化 内存泄漏检测神器。建议仅用于线下集成

它的 缺点 比较明显,具体有以下两点:

  • 1)、虽然使用了 idleHandler与多进程,可是 dumphprof 的 SuspendAll Thread 的特性依然会致使应用卡顿
  • 2)、在三星等手机,系统会缓存最后一个Activity,此时应该采用更严格的检测模式

3、Android内存管理机制回顾

ART 和 Dalvik 虚拟机使用 分页和内存映射 来管理内存。下面咱们先从Java的内存分配开始提及。

一、Java 内存分配

Java的 内存分配区域 分为以下 五部分

  • 1)、方法区:主要存放静态常量
  • 2)、虚拟机栈:Java变量引用
  • 3)、本地方法栈:native变量引用
  • 4)、堆:对象
  • 5)、程序计数器:计算当前线程的当前方法执行到多少行

二、Java 内存回收算法

一、标记-清除算法

流程可简述为 两步

  • 1)、标记全部须要回收的对象
  • 2)、统一回收全部被标记的对象

优势

实现比较简单。

缺点

  • 1)、标记、清除效率不高
  • 2)、产生大量内存碎片

二、复制算法

流程可简述为 三步

  • 1)、将内存划分为大小相等的两块
  • 2)、一块内存用完以后复制存活对象到另外一块
  • 3)、清理另外一块内存

优势

实现简单,运行高效,每次仅需遍历标记一半的内存区域

缺点

浪费一半的空间,代价大。

三、标记-整理算法

流程可简述为 三步

  • 1)、标记过程与 标记-清除算法 同样
  • 2)、存活对象往一端进行移动
  • 3)、清理其他内存

优势

  • 1)、避免 标记-清除 致使的内存碎片
  • 2)、避免复制算法的空间浪费

四、分代收集算法

如今 主流的虚拟机 通常用的比较多的仍是分代收集算法,它具备以下 特色

  • 1)、结合多种算法优点
  • 2)、新生代对象存活率低,使用 复制算法
  • 3)、老年代对象存活率高,使用 标记-整理算法

三、Android 内存管理机制

Android 中的内存是 弹性分配 的,分配值 与 最大值 受具体设备影响

对于 OOM场景 其实能够细分为以下两种:

  • 1)、内存真正不足
  • 2)、可用(被分配的)内存不足

咱们须要着重注意一下这两种的区分。

四、小结

以Android中虚拟机的角度来讲,咱们要清楚 Dalvik 与 ART 区别Dalvik 仅固定一种回收算法,而 ART 回收算法可在 运行期按需选择,而且,ART 具有 内存整理 能力,减小内存空洞

最后,LMK(Low Memory killer) 机制保证了进程资源的合理利用,它的实现原理主要是 根据进程分类和回收收益来综合决定的一套算法集

4、内存抖动

内存频繁分配和回收 致使内存 不稳定,就会出现内存抖动,它一般表现为 频繁GC、内存曲线呈锯齿状

而且,它的危害也很严重,一般会致使 页面卡顿,甚至形成 OOM

一、那么,为何内存抖动会致使 OOM?

主要缘由有以下两点:

  • 1)、频繁建立对象,致使内存不足及碎片(不连续)
  • 2)、不连续的内存片没法被分配,致使OOM

二、内存抖动解决实战

这里咱们假设有这样一个场景:点击按钮使用 handler 发送一个空消息,handler 的 handleMessage 接收到消息后建立内存抖动,即在 for 循环建立 100个容量为10万 的 strings 数组并在 30ms 后继续发送空消息。

通常使用 Memory Profiler (表现为 频繁GC、内存曲线呈锯齿状)结合代码排查便可找到内存抖动出现的地方。

一般的技巧就是着重查看 循环或频繁被调用 的地方。

三、内存抖动常见案例

下面列举一些致使内存抖动的常见案例,以下所示:

一、字符串使用加号拼接

  • 1)、使用StringBuilder替代
  • 2)、初始化时设置容量,减小StringBuilder的扩容

二、资源复用

  • 1)、使用 全局缓存池,以 重用频繁申请和释放的对象
  • 2)、注意 结束 使用后,须要 手动释放对象池中的对象

三、减小不合理的对象建立

  • 1)、ondraw、getView 中建立的对象尽可能进行复用
  • 2)、避免在循环中不断建立局部变量

四、使用合理的数据结构

使用 SparseArray类族、ArrayMap 来替代 HashMap

5、内存优化体系化搭建

在开始咱们今天正式的主题以前,咱们先来回归一下内存泄漏的概念与解决技巧。

所谓的内存泄漏就是 内存中存在已经没有用的对象。它的 表现 通常为 内存抖动、可用内存逐渐减小。 它的 危害 即会致使 内存不足、GC频繁、OOM

而对于 内存泄漏的分析 通常可简述为以下 两步

  • 1)、使用 Memory Profiler 初步观察
  • 2)、经过 Memory Analyzer 结合代码确认

一、MAT回顾

MAT查找内存泄漏

对于MAT来讲,其常规的查找内存泄漏的方式能够细分为以下三步:

  • 1)、首先,找到当前 Activity,在 Histogram 中选择其 List Objects 中的 with incoming reference(哪些引用引向了我)
  • 2)、而后,选择当前的一个 Path to GC Roots/Merge to GC Roots 的 exclude All 弱软虚引用
  • 3)、最后,找到的泄漏对象在左下角下会有一个小圆圈

此外,在 Android性能优化以内存优化 还有几种进阶的使用方式,这里就不一一赘述了,下面,咱们来看看关于 MAT 使用时的一些关键细节。

MAT的关键使用细节

要全面掌握MAT的用法,必需要先了解 隐藏在 MAT 使用中的四大细节,以下所示:

  • 1)、善于使用 Regex 查找对应泄漏类
  • 2)、使用 group by package 查找对应包下的具体类
  • 3)、明白 with outgoing references 和 with incoming references 的区别
    • with outgoing references:它引用了哪些对象
    • with incoming references:哪些对象引用了它
  • 4)、了解 Shallow Heap 和 Retained Heap 的区别
    • Shallow Heap:表示对象自身占用的内存
    • Retained Heap:对象自身占用的内存 + 对象引用的对象所占用的内存

MAT 关键组件回顾

除此以外,MAT 共有 5个关键组件 帮助咱们去分析内存方面的问题,分别以下所示:

  • 1)、Dominator_tree
  • 2)、Histogram
  • 3)、thread_overview
  • 4)、Top Consumers
  • 5)、Leak Suspects

下面咱们这里再简单地回顾一下它们。

一、Dominator(支配者):

若是从GC Root到达对象A的路径上必须通过对象B,那么B就是A的支配者。

二、Histogram和dominator_tree的区别:

  • 1)、Histogram 显示 Shallow Heap、Retained Heap、Objects,而 dominator_tree 显示的是 Shallow Heap、Retained Heap、Percentage
  • 2)、Histogram 基于 的角度,dominator_tree是基于 实例 的角度。Histogram 不会具体显示每个泄漏的对象,而dominator_tree会

三、thread_overview

查看 线程数量线程的 Shallow Heap、Retained Heap、Context Class Loader 与 is Daemon

四、Top Consumers

经过 图形 的形式列出 占用内存比较多的对象

在下方的 Biggest Objects 还能够查看其 相对比较详细的信息,例如 Shallow Heap、Retained Heap

五、Leak Suspects

列出有内存泄漏的地方,点击 Details 能够查看其产生内存泄漏的引用链

二、搭建体系化的图片优化 / 监控机制

在介绍图片监控体系的搭建以前,首先咱们来回顾下 Android Bitmap 内存分配的变化

Android Bitmap 内存分配的变化

在Android 3.0以前

  • 1)、Bitmap 对象存放在 Java Heap,而像素数据是存放在 Native 内存中的
  • 2)、若是不手动调用 recycle,Bitmap Native 内存的回收彻底依赖 finalize 函数回调,可是回调时机是不可控的

Android 3.0 ~ Android 7.0

Bitmap对象像素数据 统一放到 Java Heap 中,即便不调用 recycle,Bitmap 像素数据也会随着对象一块儿被回收。

可是,Bitmap 所有放在 Java Heap 中的缺点很明显,大体有以下两点:

  • 1)、Bitmap是内存消耗的大户,而 Max Java Heap 通常限制为 25六、512MB,Bitmap 过大过多容易致使 OOM
  • 2)、容易引发大量 GC,没有充分利用系统的可用内存

Android 8.0及之后

  • 1)、使用了可以辅助回收 Native 内存的 NativeAllocationRegistry,以实现将像素数据放到 Native 内存中,而且能够和 Bitmap 对象一块儿快速释放,最后,在 GC 的时候还能够考虑到这些 Bitmap 内存以防止被滥用
  • 2)、Android 8.0 为了 解决图片内存占用过多和图像绘制效率过慢 的问题新增了 硬件位图 Hardware Bitmap

那么,咱们如何将图片内存存放在 Native 中呢?

将图片内存存放在Native中的步骤有 四步,以下所示:

  • 1)、调用 libandroid_runtime.so 中的 Bitmap 构造函数,申请一张空的 Native Bitmap。对于不一样 Android 版本而言,这里的获取过程都有一些差别须要适配
  • 2)、申请一张普通的 Java Bitmap
  • 3)、将 Java Bitmap 的内容绘制到 Native Bitmap 中
  • 4)、释放 Java Bitmap 内存

咱们都知道的是,当 系统内存不足 的时候,LMK 会根据 OOM_adj 开始杀进程,从 后台、桌面、服务、前台,直到手机重启。而且,若是频繁申请释放 Java Bitmap 也很容易致使内存抖动。对于这种种问题,咱们该 如何评估内存对应用性能的影响 呢?

对此,咱们能够主要从如下 两个方面 进行评估,以下所示:

  • 1)、崩溃中异常退出和 OOM 的比例
  • 2)、低内存设备更容易出现内存不足和卡顿,须要查看应用中用户的手机内存在 2GB 如下所占的比例

对于具体的优化策略与手段,咱们能够从如下 七个方面 来搭建一套 成体系化的图片优化 / 监控机制

一、统一图片库

在项目中,咱们须要 收拢图片的调用,避免使用 Bitmap.createBitmap、BitmapFactory 相关的接口建立 Bitmap,而应该使用本身的图片框架

二、设备分级优化策略

内存优化首先须要根据 设备环境 来综合考虑,让高端设备使用更多的内存,作到 针对设备性能的好坏使用不一样的内存分配和回收策略

所以,咱们能够使用相似 device-year-class 的策略对设备进行分级,对于低端机用户能够关闭复杂的动画或”重功能“,使用565格式的图片或更小的缓存内存 等等。

业务开发人员须要 考虑功能是否对低端机开启,在系统资源不够时主动去作降级处理

三、创建统一的缓存管理组件

创建统一的缓存管理组件(参考 ACache),并合理使用 OnTrimMemory / LowMemory 回调,根据系统不一样的状态去释放相应的缓存与内存

在实现过程当中,须要 解决使用 static LRUCache 来缓存大尺寸 Bitmap 的问题

而且,在经过实际的测试后,发现 onTrimMemory 的 ComponetnCallbacks2.TRIM_MEMORY_COMPLETE 并不等价于 onLowMemory,所以建议仍然要去监听 onLowMemory 回调

四、低端机避免使用多进程

一个 空进程 也会占用 10MB 内存,低端机应该尽量减小使用多进程。

针对低端机用户能够推出 4MB 的轻量级版本,现在日头条极速版、Facebook Lite。

五、线下大图片检测

在开发过程当中,若是检测到不合规的图片使用(如图片宽度超过View的宽度甚至屏幕宽度),应该马上提示图片所在的Activity和堆栈,让开发人员更快发现并解决问题。在灰度和线上环境,能够将异常信息上报到后台,还能够计算超宽率(图片超过屏幕大小所占图片总数的比例)

下面,咱们介绍下如何实现对大图片的检测。

常规实现

继承 ImageView,重写实现计算图片大小。可是侵入性强,而且不通用。

所以,这里咱们介绍一种更好的方案:ARTHook。

ARTHook优雅检测大图

ARTHook,即 挂钩,用额外的代码勾住原有的方法,以修改执行逻辑,主要能够用于如下四个方面:

  • 1)、AOP编程
  • 2)、运行时插桩
  • 3)、性能分析
  • 4)、安全审计

具体咱们是使用 Epic 来进行 Hook,Epic 是 一个虚拟机层面,以 Java 方法为粒度的运行时 Hook 框架。简单来讲,它就是 ART 上的 Dexposed,而且它目前 支持 Android 4.0~10.0

Epic github 地址

使用步骤

Epic一般的使用步骤为以下三个步骤:

一、在项目 moudle 的 build.gradle 中添加

compile 'me.weishu:epic:0.6.0'
复制代码

二、继承 XC_MethodHook,实现 Hook 方法先后的逻辑。如 监控Java线程的建立和销毁

class ThreadMethodHook extends XC_MethodHook{
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Thread t = (Thread) param.thisObject;
        Log.i(TAG, "thread:" + t + ", started..");
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Thread t = (Thread) param.thisObject;
        Log.i(TAG, "thread:" + t + ", exit..");
    }
}
复制代码

三、注入 Hook 好的方法:

DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
复制代码

知道了 Epic 的基本使用方法以后,咱们即可以利用它来实现大图片的监控报警了。

项目实战

Awesome-WanAndroid 项目为例,首先,在 WanAndroidApp 的 onCreate 方法中添加以下代码:

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            super.afterHookedMethod(param);
        // 1
        DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
        }
    });
复制代码

在注释1处,咱们 经过调用 DexposedBridge 的 findAndHookMethod 方法找到全部经过 ImageView 的 setImageBitmap 方法设置的切入点,其中最后一个参数 ImageHook 对象是继承了 XC_MethodHook 类,其目的是为了 重写 afterHookedMethod 方法拿到相应的参数进行监控逻辑的判断

接下来,咱们来实现咱们的 ImageHook 类,代码以下所示:

public class ImageHook extends XC_MethodHook {

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        // 1
        ImageView imageView = (ImageView) param.thisObject;
        checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
    }


    private static void checkBitmap(Object thiz, Drawable drawable) {
        if (drawable instanceof BitmapDrawable && thiz instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                final View view = (View) thiz;
                int width = view.getWidth();
                int height = view.getHeight();
                if (width > 0 && height > 0) {
                    // 二、图标宽高都大于view的2倍以上,则警告
                    if (bitmap.getWidth() >= (width << 1)
                        &&  bitmap.getHeight() >= (height << 1)) {
                    warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                }
                } else {
                    // 三、当宽高度等于0时,说明ImageView尚未进行绘制,使用ViewTreeObserver进行大图检测的处理。
                    final Throwable stackTrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int w = view.getWidth();
                            int h = view.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1)
                                    && bitmap.getHeight() >= (h << 1)) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
                                }
                                view.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }

    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = "Bitmap size too large: " +
            "\n real size: (" + bitmapWidth + ',' + bitmapHeight + ')' +
            "\n desired size: (" + viewWidth + ',' + viewHeight + ')' +
            "\n call stack trace: \n" + Log.getStackTraceString(t) + '\n';

        LogHelper.i(warnInfo);
    }
}
复制代码

首先,在注释1处,咱们重写了 ImageHook 的 afterHookedMethod 方法,拿到了当前的 ImageView 和要设置的 Bitmap 对象。而后,在注释2处,若是当前 ImageView 的宽高大于0,咱们便进行大图检测的处理:ImageView 的宽高都大于 View 的2倍以上,则警告。接着,在注释3处,若是当前 ImageView 的宽高等于0,则说明 ImageView 尚未进行绘制,则使用 ImageView 的 ViewTreeObserver 获取其宽高进行大图检测的处理。至此,咱们的大图检测检测组件就已经实现了。若是有小伙伴对 epic 的实现原理感兴趣的,能够查看这篇文章

ARTHook方案实现小结
  • 1)、无侵入性
  • 2)、通用性强
  • 3)、兼容性问题大,开源方案不能带到线上环境

六、线下重复图片检测

首先咱们来了解一下这里的 重复图片 所指的概念: 即 Bitmap 像素数据彻底一致,可是有多个不一样的对象存在

重复图片检测的原理其实就是 使用内存 Hprof 分析工具,自动将重复 Bitmap 的图片和引用堆栈输出

已彻底配置好的项目请参见这里

使用说明

使用很是简单,只须要修改 Main 类的 main 方法的第一行代码,以下所示:

// 设置咱们本身 App 中对应的 hprof 文件路径
String dumpFilePath = "//Users//quchao//Documents//heapdump//memory-40.hprof";
复制代码

而后,咱们执行 main 方法便可在 //Users//quchao//Documents//heapdump 这个路径下看到生成的 images 文件夹,里面保存了项目中检测出来的重复的图片。images 目录以下所示:

注意:须要使用 8.0 如下的机器,由于 8.0 及之后 Bitmap 中的 buffer 已保存在 native 内存之中。

实现步骤

具体的实现能够细分为以下三个步骤:

  • 1)、首先,获取 android.graphics.Bitmap 实例对象的 mBuffer 做为 ArrayInstance ,经过 getValues 获取的数据为 Object 类型。因为后面计算 md5 须要为 byte[] 类型,因此经过反射的方式调用 ArrayInstance#asRawByteArray 直接返回 byte[] 数据
  • 2)、而后,根据 mBuffer 的数据生成 png 图片文件,这里直接参考了 github.com/JetBrains/a… 的实现方式。
  • 3)、最后,获取堆栈信息,直接 使用LeakCanary 获取 stack 的方法,使用 leakcanary-analyzer-1.6.2.jar 和 leakcanary-watcher-1.6.2.jar 这两个库文件。并用 反射 的方式调用了 HeapAnalyzer#findLeakTrace 方法。

其中,获取堆栈 的信息也能够直接使用 haha 库来进行获取。这里简单说一下 使用 haha 库获取堆栈的流程,其具体能够细分为八个步骤,以下所示:

  • 1)、首先,预备一个已经存在重复 bitmap 的 hprof 文件
  • 2)、利用 haha 库上的 MemoryMappedFileBuffer 读取 hrpof 文件 [关键代码 new MemoryMappedFileBuffer(heapDumpFile) ]
  • 3)、解析生成 snapshot,获取 heap,这里我只获取了 app heap [关键代码 snapshot.getHeaps(); heap.getName().equals("app") ]
  • 4)、从 snapshot 中根据指定 class 查找出全部的 Bitmap Classes [关键代码snapshot.findClasses(Bitmap.class.getName()) ]
  • 5)、从 heap 中得到全部的 Bitmap 实例 instance [关键代码 clazz.getHeapInstances(heap.getId()) ]
  • 6)、根据 instance 中获取全部的属性信息 Field[],并从 Field[] 查找出咱们须要的 "mWidth" "mHeight" "mBuffer" 信息
  • 7)、经过 "mBuffer" 属性便可获取到他们的 hashcode 来判断是不是重复图片
  • 8)、最后,经过 instance 中 mNextInstanceToGcRoot 获取整个引用链信息并打印

七、创建全局的线上 Bitmap 监控

为了创建全局的 Bitmap 监控,咱们必须 对 Bitmap 的分配和回收 进行追踪。咱们先来看看 Bitmap 有哪些特色:

  • 1)、建立场景比较单一:在 Java 层调用 Bitmap.create 或 BitmapFactory 等方法建立,能够封装一层对 Bitmap 建立的接口,注意要 包含调用第三方库产生的 Bitmap,这里咱们具体能够使用 编译插桩 + ASM 的方式来高效地实现。
  • 2)、建立频率比较低
  • 3)、和 Java 对象的生命周期同样服从 GC,能够使用 WeakReference 来追踪 Bitmap 的销毁

根据以上特色,咱们能够创建一套 Bitmap 的高性价比监控组件

  • 1)、首先,在接口层将全部建立出来的 Bitmap 放入一个 WeakHashMap 中,并记录建立 Bitmap 的数据、堆栈等信息。
  • 2)、而后,每隔必定时间查看 WeakHashMap 中有哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。
  • 3)、最后,若是发生了 Bitmap 滥用或泄露,则将相关的数据与堆栈等信息打印出来或上报至 APM 后台。

这个方案的 性能消耗很低,能够在 正式环境 中进行。可是,须要注意的一点是,正式与测试环境须要采用不一样程度的监控。

三、创建线上应用内存监控体系

要创建线上应用的内存监控体系,咱们须要 先获取 App 的 DalvikHeap 与 NativeHeap,它们的获取方式可归结为以下四个步骤:

  • 一、首先,经过 ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 获取内存信息数据
  • 二、而后,经过 hook Debug.MemoryInfo 的 getMemoryStat 方法(os v23 及以上)能够得到 Memory Profiler 中的多项数据,进而得到 细份内存的使用状况
  • 三、接着,经过 Runtime 获取 DalvikHeap
  • 四、最后,经过 Debug.getNativeHeapAllocatedSize 获取 NativeHeap

对于监控场景,咱们须要将其划分为两大类,以下所示:

  • 1)、常规内存监控
  • 2)、低内存监控

一、常规内存监控

根据 斐波那契数列 每隔一段时间(max:30min)获取内存的使用状况。常规内存的监控方法有多种实现方式,下面,咱们按照 项目早期 => 壮大期 => 成熟期 的常规内存监控方式进行 演进式 讲解。

项目早期:针对场景进行线上 Dump 内存的方式

具体使用 Debug.dumpHprofData() 实现。

其实现的流程为以下四个步骤:

  • 1)、超过最大内存的 80%
  • 2)、内存 Dump
  • 3)、回传文件至服务器
  • 4)、MAT 手动分析

可是,这种方式有以下几个缺点:

  • 1)、Dump文件太大,和对象数正相关,能够进行裁剪
  • 2)、上传失败率高,分析困难

壮大期:LeakCanary带到线上的方式

在使用 LeakCanary 的时候咱们须要 预设泄漏怀疑点,一旦发现泄漏进行回传。但这种实现方式缺点比较明显,以下所示:

  • 1)、不适合全部状况,须要预设怀疑点
  • 2)、分析比较耗时,容易致使 OOM

成熟期:定制 LeakCanary 方式

那么,如何定制线上的LeakCanary?

定制 LeakCanary 其实就是对 haha组件 来进行 定制。haha库是 square 出品的一款 自动分析Android堆栈的java库。这是haha库的 连接地址

对于haha库,它的 基本用法 通常遵循为以下四个步骤:

一、导出堆栈文件

File heapDumpFile = ...
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
复制代码

二、根据堆栈文件建立出内存映射文件缓冲区

DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
复制代码

三、根据文件缓存区建立出对应的快照

Snapshot snapshot = Snapshot.createSnapshot(buffer);
复制代码

四、从快照中获取指定的类

ClassObj someClass = snapshot.findClass("com.example.SomeClass");
复制代码

咱们在实现线上版的LeakCanary的时候主要要解决的问题有三个,以下所示:

  • 1)、解决 预设怀疑点 时不许确的问题 => 自动找怀疑点
  • 2)、解决掉将 hprof 文件映射到内存中的时候可能致使内存暴涨甚至发生 OOM 的问题 => 对象裁剪,不所有加载到内存。即对生成的 Hprof 内存快照文件作一些优化:裁剪大部分图片对应的 byte 数据 以减小文件开销,最后,使用 7zip 压缩,通常可 节省 90% 大小
  • 3)、分析泄漏链路慢而致使分析时间过长 => 分析 Retain size 大的对象

成熟期:实现内存泄漏监控闭环

在实现了线上版的 LeakCanary 以后,就须要 将线上版的 LeakCanary 与服务器和前端页面结合 起来。具体的 内存泄漏监控闭环流程 以下所示:

  • 1)、当在线上版 LeakCanary 上发现内存泄漏时,手机将上传内存快照至服务器
  • 2)、此时服务器分析 Hprof,若是不是系统缘由致使误报则经过 git 获得该最近修改人
  • 3)、最后将内存泄漏 bug 单提交给负责人。该负责人经过前端实现的 bug 单系统便可看到本身新增的bug

此外,在实现 图片内存监控 的过程当中,应注意 两个关键点,以下所示:

  • 1)、在线上能够按照 不一样的系统、屏幕分辨率 等纬度去 分析图片内存的占用状况
  • 2)、在 OOM 崩溃时,能够将 图片总内存、Top N 图片占用内存 写入 崩溃日志

二、低内存监控

对于低内存的监控,一般有两种方式,分别以下所示:

  • 一、利用 onTrimMemory / onLowMemory 监听系统回调的物理内存警告
  • 二、在后台起一个服务定时监控系统的内存占用,只要超过虚拟内存大小最大限制的 90% 则直接触发内存警告

三、内存监控指标

为了准确衡量内存性能,咱们须要引入一系列的内存监控指标,以下所示:

1)、发生频率

2)、发生时各项内存使用情况

3)、发生时App的当前场景

4)、内存异常率

内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集UV
PSS 获取:调用 Debug.MemoryInfo 的 API 便可
复制代码

若是出现 新的内存使用不当或内存泄漏 的场景,这个指标会有所 上涨

5)、触顶率

内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集UV
复制代码

计算触顶率的代码以下所示:

long javaMax = Runtime.maxMemory();
long javaTotal = Runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
float proportion = (float) javaUsed / javaMax;
复制代码

若是超过 85% 最大堆 的限制,GC 会变得更加 频发,容易形成 OOM 和 卡顿

四、小结

在具体实现的时候,客户端 尽可能只负责 上报数据,而 指标值的计算 能够由 后台 来计算。这样即可以经过 版本对比监控是否有 新增内存问题。所以,创建线上内存监控的完整方案 至少须要包含如下四点

  • 1)、待机内存、重点模块内存、OOM率
  • 2)、总体及重点模块 GC 次数、GC 时间
  • 3)、加强的 LeakCanry 自动化内存泄漏分析
  • 4)、低内存监控模块的设置

四、创建全局的线程监控组件

每一个线程初始化都须要 mmap 必定的栈大小,在默认状况下初始化一个线程须要 mmap 1MB 左右的内存空间

32bit 的应用中有 4g 的 vmsize实际能使用的有 3g+,这样一个进程 最大能建立的线程数 能够达到 3000个,可是,linux 对每一个进程可建立的线程数也有必定的限制(/proc/pid/limits),而且,不一样厂商也能修改这个限制,超过该限制就会 OOM。

所以,对线程数量的限制,在必定程度上能够 有效地避免 OOM 的发生。那么,实现一套 全局的线程监控组件 即是 刻不容缓 的了。

全局线程监控组件的实现原理

在线下或灰度的环境下经过一个定时器每隔 10分钟 dump 出应用全部的线程相关信息,当线程数超过当前阈值时,则将当前的线程信息上报并预警

五、GC 监控组件搭建

经过** Debug.startAllocCounting** 来监控 GC 状况,注意有必定 性能影响

Android 6.0 以前 能够拿到 内存分配次数和大小以及 GC 次数,其对应的代码以下所示:

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
复制代码

而且,在 Android 6.0 及以后 能够拿到 更精准GC 信息:

Debug.getRuntimeStat("art.gc.gc-count");
Debug.getRuntimeStat("art.gc.gc-time");
Debug.getRuntimeStat("art.gc.blocking-gc-count");
Debug.getRuntimeStat("art.gc.blocking-gc-time");
复制代码

对于 GC 信息的排查,咱们通常关注 阻塞式GC的次数和耗时,由于它会 暂停线程,可能致使应用发生 卡顿。建议 仅对重度场景使用

六、创建线上 OOM 监控组件:Probe

美团的 Android 内存泄漏自动化链路分析组件 ProbeOOM 时会生成 Hprof 内存快照,而后,它会经过 单独进程 对这个 文件 作进一步 分析

Probe 组件的缺陷及解决方案

它的缺点比较多,具体为以下几点:

  • 一、在崩溃的时候生成内存快照容易致使二次崩溃
  • 二、部分手机生成 Hprof 快照比较耗时
  • 三、部分 OOM 是由虚拟内存不足致使

在实现自动化链路分析组件 Probe 的过程当中主要要解决两个问题,以下所示:

一、链路分析时间过长

  • 1)、使用链路归并:将具备 相同层级与结构 的链路进行 合并
  • 2)、使用 自适应扩容法经过不断比较现有链路和新链路,结合扩容因子,逐渐完善为完整的泄漏链路

二、分析进程占用内存过大

分析进程占用的内存内存快照文件的大小 不成正相关,而跟 内存快照文件的 Instance 数量正相关。因此在开发过程当中咱们应该 尽量排除不须要的Instance实例

Prope 分析流程揭秘

Prope 的 整体架构图 以下所示:

image

而它的整个分析流程具体能够细分为八个步骤,以下所示:

一、hprof 映射到内存 => 解析成 Snapshot & 计数压缩:

解析后的 Snapshot 中的 Heap 有四种类型,具体为:

  • 1)、DefaultHeap
  • 2)、ImageHeap
  • 3)、App Heap:包括 ClassInstance、ClassObj、ArrayInstance、RootObj
  • 4)、System Heap

解析完 后使用了 计数压缩策略,对 相同的 Instance 使用 计数,以 减小占用内存。超过计数阈值的须要计入计数桶(计数桶记录了 丢弃个数 和 每一个 Instance 的大小)

二、生成 Dominator Tree

三、计算 RetainSize

四、生成 Reference 链 && 基础数据类型加强:

若是对象是 基础数据类型,会将 自身的 RetainSize 累加到父节点 上,将 怀疑对象 替换为它的 父节点

五、链路归并

六、计数桶补偿 & 基础数据类型和父节点融合

使用计数补偿策略计算 RetainSize,主要是 判断对象是否在计数桶中,若是在的话则将 丢弃的个数和大小补偿到对象上,累积计算RetainSize,最后对 RetainSize 排序以查找可疑对象

七、排序扩容

八、查找泄露链路

七、实现 单机版 的 Profile Memory 自动化内存分析

项目地址请点击此处

在配置的时候要注意两个问题:

  • 一、liballoc-lib.so在构建后工程的 build => intermediates => cmake 目录下。将对应的 cpu abi 目录拷贝到新建的 libs 目录下

  • 二、在 DumpPrinter Java 库的 build.gradle 中的 jar 闭包中须要加入如下代码以识别源码路径:

    sourceSets.main.java.srcDirs = ['src']

使用步骤

具体的使用步骤以下所示:

一、首先,点击 ”开始记录“ 按钮能够看到触发对象分配的记录,说明对象已经开始记录对象的分配,log以下所示:

12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
复制代码

二、而后,点击屡次 ”生成1000个对象“ 按钮,当对象达到设置的最大数量的时候触发内存dump,会获得保存数据路径的日志。以下所示:

12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
12-26 10:56:45.103 30450-30450/com.dodola.alloctrack I/AllocTracker: saveARTAllocationData write file to /storage/emulated/0/crashDump/1577329005
复制代码

三、此时,能够看到数据保存在 sdk 下的 crashDump 目录下。

四、接着,经过 gradle task :buildAlloctracker 任务编译出存放在 tools/DumpPrinter-1.0.jar 的 dump 工具,而后采用以下命令来将数据解析 到dump_log.txt 文件中。

java -jar tools/DumpPrinter-1.0.jar dump文件路径 > dump_log.txt
复制代码

五、最后,就能够在 dump_log.txt 文件中看到解析出来的数据,以下所示:

Found 4949 records:
tid=1 byte[] (94208 bytes)
    dalvik.system.VMRuntime.newNonMovableArray (Native method)
    android.graphics.Bitmap.nativeCreate (Native method)
    android.graphics.Bitmap.createBitmap (Bitmap.java:975)
    android.graphics.Bitmap.createBitmap (Bitmap.java:946)
    android.graphics.Bitmap.createBitmap (Bitmap.java:913)
    android.graphics.drawable.RippleDrawable.updateMaskShaderIfNeeded (RippleDrawable.java:776)
    android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:860)
    android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:700)
    android.view.View.getDrawableRenderNode (View.java:17736)
    android.view.View.drawBackground (View.java:17660)
    android.view.View.draw (View.java:17467)
    android.view.View.updateDisplayListIfDirty (View.java:16469)
    android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
    android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:3885)
    android.view.View.updateDisplayListIfDirty (View.java:16429)
    android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
复制代码

八、搭建线下 Native 内存泄漏监控体系

Android 8.0 及以后,能够使用 Address Sanitizer、Malloc 调试和 Malloc 钩子 进行 native 内存分析,参见 native_memory

对于线下 Native 内存泄漏监控的创建,主要针对 是否能重编 so 的状况 来记录分配的内存信息。

针对没法重编so的状况

  • 1)、首先,使用 PLT Hook 拦截库的内存分配函数,而后,重定向到咱们本身的实现后去 记录分配的 内存地址、大小、来源so库路径 等信息。
  • 2)、最后,按期 扫描分配与释放 的配对内存块,对于 不配对的分配 输出上述记录的信息

针对可重编的so状况

  • 1)、首先,经过 GCC”-finstrument-functions“ 参数给 全部函数插桩,而后,在桩中模拟调用栈的入栈与出栈操做
  • 2)、接着,经过 ld”--warp“ 参数 拦截内存分配和释放函数,重定向到咱们本身的实现后记录分配的 内存地址、大小、来源so以及插桩调用栈此刻的内容
  • 3)、最后,按期扫描分配与释放是否配对,对于不配对的分配输出咱们记录的信息

九、设置内存兜底策略

设置内存兜底策略的目的,是为了 在用户无感知的状况下,在接近触发系统异常前,选择合适的场景杀死进程并将其重启,从而使得应用内存占用回到正常状况

一般执行内存兜底策略时至少须要知足六个条件,以下所示:

  • 1)、是否在主界面退到后台且位于后台时间超过 30min
  • 2)、当前时间为早上 2~5 点
  • 3)、不存在前台服务(通知栏、音乐播放栏等状况)
  • 4)、Java heap 必须大于当前进程最大可分配的85% || native内存大于800MB
  • 5)、vmsize 超过了4G(32bit)的85%
  • 6)、非大量的流量消耗(不超过1M/min) && 进程无大量CPU调度状况

只有在知足了以上条件以后,咱们才会去杀死当前主进程并经过 push 进程从新拉起及初始化

十、更深刻的内存优化策略

除了在 Android性能优化以内存优化 => 优化内存空间 中讲解过的一些常规的内存优化策略之外,在下面列举了一些更深刻的内存优化策略。

一、使 bitmap 资源在 native 中分配

对于 Android 2.x 系统,使用反射将 BitmapFactory.Options 里面隐藏的 inNativeAlloc 打开

对于 Android 4.x 系统,使用或借鉴 Fresco 将 bitmap 资源在 native 中分配的方式

二、图片加载时的降级处理

使用 Glide、Fresco 等图片加载库,经过定制,在加载 bitmap 时,若发生 OOM,则使用 try catch 将其捕获,而后清除图片 cache,尝试下降 bitmap format(ARGB888八、RGB56五、ARGB444四、ALPHA8)。

须要注意的是,OOM 是能够捕获的,只要 OOM 是由 try 语句中的对象声明所致使的,那么在 catch 语句中,是能够释放掉这些对象,解决 OOM 的问题的。

三、前台每隔 3 分钟去获取当前应用内存占最大内存的比例,超过设定的危险阈值(如80%)则主动释放应用 cache(Bitmap 为大头),而且显示地除去应用的 memory,以加速内存收集的过程。

计算当前应用内存占最大内存的比例的代码以下:

max = Runtime.getRuntime().maxMemory();
available = Runtime.getRuntime.totalMemory() - Runtime.getFreeMemory();
ratio = available / max;
复制代码

显示地除去应用的 memory,以加速内存收集过程的代码以下所示:

WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
复制代码

五、因为 webview 存在内存系统泄漏,还有 图库占用内存过多 的问题,能够采用单独的进程。

六、当UI隐藏时释放内存

当用户切换到其它应用而且你的应用 UI 再也不可见时,应该释放应用 UI 所占用的全部内存资源。这可以显著增长系统缓存进程的能力,可以提高用户体验。

在全部 UI 组件都隐藏的时候会接收到 Activity 的 onTrimMemory() 回调并带有参数 TRIM_MEMORY_UI_HIDDEN

七、Activity 的兜底内存回收策略

在 Activity 的 onDestory 中递归释放其引用到的 Bitmap、DrawingCache 等资源,以下降发生内存泄漏时对应用内存的压力。

八、使用相似 Hack 的方式修复系统内存泄漏

LeakCanary 的 AndroidExcludeRefs 列出了一些因为系统缘由致使引用没法释放的例子,可以使用相似 Hack 的方式去修复。具体的实现代码能够参考 Booster => 系统问题修复

九、应用发生 OOM 时,须要上传更加详细的内存相关信息。

十、当应用使用的Service再也不使用时应该销毁它,建议使用 IntentServcie。

十一、谨慎使用第三方库,避免为了使用其中一两个功能而导入一个大而全的解决方案。

6、内存优化演进

一、自动化测试阶段

内存达到阈值后自动触发 Hprof Dump,将获得的 Hprof 存档后由人工经过 MAT 进行分析。

二、LeakCanary

检测和分析报告都在一块儿,批量自动化测试和过后分析都不太方便。

三、使用基于 LeakCannary 的改进版 ResourceCanary

Matrix => ResourceCanary 实现原理

主要功能

目前,它的主要功能有 三个部分,以下所示:

一、分离 检测和分析 两部分流程

自动化测试由测试平台进行,分析则由监控平台的服务端离线完成,最后再通知相关开发解决问题。

二、裁剪 Hprof文件,以下降 传输 Hprof 文件与后台存储 Hprof 文件的开销

获取 须要的类和对象相关的字符串 信息便可,其它数据均可以在客户端裁剪,通常能 Hprof 大小会减少至原来的 1/10 左右。

三、增长重复 Bitmap 对象检测

方便经过减小冗余 Bitmap 的数量,以下降内存消耗。

四、小结

在研发阶段须要不断实现 更多的工具和组件,以此系统化地提高自动化程度,以最终 提高发现问题的效率

7、内存优化工具

除了经常使用的内存分析工具 Memory Profiler、MAT、LeakCanary 以外,还有一些其它的内存分析工具,下面我将一一为你们进行介绍。

一、top

top 命令是 Linux 下经常使用的性能分析工具,可以 实时显示系统中各个进程的资源占用情况,相似于 Windows 的任务管理器。top 命令提供了 实时的对系统处理器的状态监视。它将 显示系统中 CPU 最“敏感”的任务列表。该命令能够按 CPU使用、内存使用和执行时间 对任务进行排序

接下来,咱们输入如下命令查看top命令的用法:

quchao@quchaodeMacBook-Pro ~ % adb shell top --help
usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-d SECONDS] [-p PID,] [-u USER,]

Show process activity in real time.

-H	Show threads
-k	Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o	Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O	Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S from default)
-s	Sort by field number (1-X, default 9)
-b	Batch mode (no tty)
-d	Delay SECONDS between each cycle (default 3)
-n	Exit after NUMBER iterations
-p	Show these PIDs
-u	Show these USERs
-q	Quiet (no header lines)

Cursor LEFT/RIGHT to change sort, UP/DOWN move list, space to force
update, R to reverse sort, Q to exit.
复制代码

这里使用 top 仅显示一次进程信息,以便来说解进程信息中各字段的含义。

image

总体的统计信息区

前四行 是当前系统状况 总体的统计信息区。下面咱们看每一行信息的具体意义。

第一行:Tasks — 任务(进程)

具体信息说明以下所示:

系统如今共有 729 个进程,其中处于 运行中 的有 1 个,715 个在 休眠(sleep)stoped 状态的有0个,zombie 状态(僵尸)的有 8 个。

第二行:内存状态

具体信息以下所示:

  • 1)、5847124k total:物理内存总量(5.8GB)
  • 2)、5758016k used:使用中的内存总量(5.7GB)
  • 3)、89108k free:空闲内存总量(89MB)
  • 4)、112428k buffers:缓存的内存量 (112M)

第三行:swap交换分区信息

具体信息说明以下所示:

  • 1)、2621436k total:交换区总量(2.6GB)
  • 2)、612572k used:使用的交换区总量(612MB)
  • 3)、2008864k free:空闲交换区总量(2GB)
  • 4)、2657696k cached:缓冲的交换区总量(2.6GB)

第四行:cpu状态信息

具体属性说明以下所示:

  • 1)、800% cpu:8核 CPU
  • 2)、39% user:39% CPU被用户进程使用
  • 3)、0% nice:优先值为负的进程占 0%
  • 4)、42% sys — 内核空间占用 CPU 的百分比为 42%
  • 5)、712% idle:除 IO 等待时间之外的其它等待时间为 712%
  • 6)、0% iow:IO 等待时间占 0%
  • 7)、0% irq:硬中断时间占 0%
  • 8)、6% sirq - 软中断时间占 0%

对于内存监控,在 top 里咱们要时刻监控 第三行 swap 交换分区的 used,若是这个数值在不断的变化,说明内核在不断进行内存和 swap 的数据交换,这是真正的内存不够用了。

进程(任务)的状态监控

第五行及如下,就是各进程(任务)的状态监控,项目列信息说明以下所示:

  • 1)、PID:进程 id
  • 2)、USER:进程全部者
  • 3)、PR:进程优先级
  • 4)、NI:nice 值。负值表示高优先级,正值表示低优先级
  • 5)、VIRT:进程使用的虚拟内存总量。VIRT = SWAP + RES
  • 6)、RES:进程使用的、未被换出的物理内存大小。RES = CODE + DATA
  • 7)、SHR:共享内存大小
  • 8)、S:进程状态。D = 不可中断的睡眠状态、R = 运行、 S = 睡眠、T = 跟踪 / 中止、Z = 僵尸进程
  • 9)、%CPU — 上次更新到如今的 CPU 时间占用百分比
  • 10)、%MEM:进程使用的物理内存百分比
  • 11)、TIME+:进程使用的 CPU 时间总计,单位 1/100秒
  • 12)、ARGS:进程名称(命令名 / 命令行)

从上图中能够看到,第一行的就是 Awesome-WanAndroid 这个应用的进程,它的进程名称为 json.chao.com.w+,PID 为 23104,进程全部者 USER 为 u0_a714,进程优先级 PR 为 10,nice 置 NI 为 -10。进程使用的虚拟内存总量 VIRT 为 4.3GB,进程使用的、未被换出的物理内存大小 RES 为138M,共享内存大小 SHR 为 66M,进程状态 S 是睡眠状态,上次更新到如今的 CPU 时间占用百分比 %CPU 为 21.2。进程使用的物理内存百分比 %MEM 为 2.4%,进程使用的 CPU 时间 TIME+ 为 1:47.58 / 100小时。

二、dumpsys meminfo

四大内存指标

在讲解 dumpsys meminfo 命令以前,咱们必须先了解下 Android 中最重要的 四大内存指标 的概念,以下表所示:

内存指标 英文全称 含义 等价
USS Unique Set Size 物理内存 进程独占的内存
PSS Proportional Set Size 物理内存 PSS = USS + 按比例包含共享库
RSS Resident Set Size 物理内存 RSS= USS+ 包含共享库
VSS Virtual Set Size 虚拟内存 VSS= RSS+ 未分配实际物理内存

从上可知,它们之间内存的大小关系为 VSS >= RSS >= PSS >= USS

RSS 与 PSS 类似,也包含进程共享内存,但比较麻烦的是 RSS 并无把共享内存大小全都平分到使用共享的进程头上,以致于全部进程的 RSS 相加会超过物理内存不少。而 VSS 是虚拟地址,它的上限与进程的可访问地址空间有关,和当前进程的内存使用关系并不大。好比有不少的 map 内存也被算在其中,咱们都知道,file 的 map 内存对应的多是一个文件或硬盘,或者某个奇怪的设备,它与进程使用内存并无多少关系。

PSS、USS 最大的不一样在于 “共享内存“(好比两个 App 使用 MMAP 方式打开同一个文件,那么打开文件而使用的这部份内存就是共享的),USS不包含进程间共享的内存,而PSS包含。这也形成了USS由于缺乏共享内存,全部进程的USS相加要小于物理内存大小的缘由。

最先的时候官方就推荐使用 PSS 曲线图来衡量 App 的物理内存占用,而 Android 4.4 以后才加入 USS。可是 PSS,有个很大的问题,就是 ”共享内存“,考虑一种状况,若是 A 进程与 B 进程都会使用一个共享 SO 库,那么 So 库中初始化所用掉的那部份内存就会平分到 A 与 B 的头上。可是 A 是在 B 以后启动的,那么对于 B 的 PSS 曲线而言,在 A 启动的那一刻,即便 B 没有作任何事情,也会出现一个比较大的阶梯状下滑,这会给用曲线图分析软件内存的行为形成致命的麻烦

USS 虽然没有这个问题,可是因为 Dalvik 虚拟机申请内存牵扯到 GC 时延和多种 GC 策略,这些都会影响到曲线的异常波动。例如异步 GC 是 Android 4.0 以上系统很重要的特性,可是 GC 何时结束?曲线何时”下降“?就 没法预计 了。还有 GC 策略,何时开始增长 Dalvik 虚拟机的预申请内存大小(Dalvik 启动时是有一个标称的 start 内存大小,它是为 Java 代码运行时预留的,避免 Java 运行时再申请而形成卡顿),可是这个 预申请大小是动态变化的,这一点也会 形成 USS 忽大忽小

dumpsys meminfo 命令解析

了解完 Android 内存的性能指标以后,下面咱们便来讲说 dumpsys meminfo 这个命令的用法,首先咱们输入 adb shell dumpsys meminfo -h 查看它的帮助文档:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo -h
meminfo dump options: [-a] [-d] [-c] [-s] [--oom] [process]
-a: include all available information for each process.
-d: include dalvik details.
-c: dump in a compact machine-parseable representation.
-s: dump only summary of application memory usage.
-S: dump also SwapPss.
--oom: only show processes organized by oom adj.
--local: only collect details locally, don't call process.
--package: interpret process arg as package, dumping all
            processes that have loaded that package.
--checkin: dump data for a checkin
If [process] is specified it can be the name or
pid of a specific process to dump.
复制代码

接着,咱们之间输入adb shell dumpsys meminfo命令:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 257501238 Realtime: 257501238

// 根据进程PSS占用值从大到小排序
Total PSS by process:
    308,049K: com.tencent.mm (pid 3760 / activities)
    225,081K: system (pid 2088)
    189,038K: com.android.systemui (pid 2297 / activities)
    188,877K: com.miui.home (pid 2672 / activities)
    176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
    175,231K: json.chao.com.wanandroid (pid 23104 / activities)
    126,918K: com.tencent.mobileqq (pid 23741)
    ...

// 以oom来划分,会详细列举全部的类别的进程
Total PSS by OOM adjustment:
    432,013K: Native
        76,700K: surfaceflinger (pid 784)
        59,084K: android.hardware.camera.provider@2.4-service (pid 743)
        26,524K: transport (pid 23418)
        25,249K: logd (pid 597)
        11,413K: media.codec (pid 1303)
        10,648K: rild (pid 1304)
        9,283K: media.extractor (pid 1297)
        ...
        
    661,294K: Persistent
        225,081K: system (pid 2088)
        189,038K: com.android.systemui (pid 2297 / activities)
        103,050K: com.xiaomi.finddevice (pid 3134)
        39,098K: com.android.phone (pid 2656)
        25,583K: com.miui.daemon (pid 3078)
        ...
        
    219,795K: Foreground
        175,231K: json.chao.com.wanandroid (pid 23104 / activities)
        44,564K: com.miui.securitycenter.remote (pid 2986)
        
    246,529K: Visible
        71,002K: com.sohu.inputmethod.sogou.xiaomi (pid 4820)
        52,305K: com.miui.miwallpaper (pid 2579)
        40,982K: com.miui.powerkeeper (pid 3218)
        24,604K: com.miui.systemAdSolution (pid 7986)
        14,198K: com.xiaomi.metoknlp (pid 3506)
        13,820K: com.miui.voiceassist:core (pid 8722)
        13,222K: com.miui.analytics (pid 8037)
        7,046K: com.miui.hybrid:entrance (pid 7922)
        5,104K: com.miui.wmsvc (pid 7887)
        4,246K: com.android.smspush (pid 8126)
        
    213,027K: Perceptible
        89,780K: com.eg.android.AlipayGphone (pid 8238)
        49,033K: com.eg.android.AlipayGphone:push (pid 8204)
        23,181K: com.android.thememanager (pid 11057)
        13,253K: com.xiaomi.joyose (pid 5558)
        10,292K: com.android.updater (pid 3488)
        9,807K: com.lbe.security.miui (pid 23060)
        9,734K: com.google.android.webview:sandboxed_process0 (pid 11150)
        7,947K: com.xiaomi.location.fused (pid 3524)
        
    308,049K: Backup
        308,049K: com.tencent.mm (pid 3760 / activities)
        
    74,250K: A Services
        59,701K: com.tencent.mm:push (pid 7234)
        9,247K: com.android.settings:remote (pid 27053)
        5,302K: com.xiaomi.drivemode (pid 27009)
        
    199,638K: Home
        188,877K: com.miui.home (pid 2672 / activities)
        10,761K: com.miui.hybrid (pid 7945)
        
    53,934K: B Services
        35,583K: com.tencent.mobileqq:MSF (pid 14119)
        6,753K: com.qualcomm.qti.autoregistration (pid 8786)
        4,086K: com.qualcomm.qti.callenhancement (pid 26958)
        3,809K: com.qualcomm.qti.StatsPollManager (pid 26993)
        3,703K: com.qualcomm.qti.smcinvokepkgmgr (pid 26976)
        
    692,588K: Cached
        176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
        126,918K: com.tencent.mobileqq (pid 23741)
        72,928K: com.tencent.mm:tools (pid 18598)
        68,208K: com.tencent.mm:sandbox (pid 27333)
        55,270K: com.tencent.mm:toolsmp (pid 18842)
        24,477K: com.android.mms (pid 27192)
        23,865K: com.xiaomi.market (pid 27825)
        ...

// 按内存的类别来进行划分
Total PSS by category:
    957,931K: Native
    284,006K: Dalvik
    199,750K: Unknown
    193,236K: .dex mmap
    191,521K: .art mmap
    110,581K: .oat mmap
    101,472K: .so mmap
    94,984K: EGL mtrack
    87,321K: Dalvik Other
    84,924K: Gfx dev
    77,300K: GL mtrack
    64,963K: .apk mmap
    17,112K: Other mmap
    12,935K: Ashmem
     3,364K: Stack
     2,343K: .ttf mmap
     1,375K: Other dev
     1,071K: .jar mmap
        20K: Cursor
         0K: Other mtrack

// 手机总体内存使用状况
Total RAM: 5,847,124K (status normal)
Free RAM: 3,711,324K (  692,588K cached pss + 2,428,616K cached kernel +   117,492K cached ion +   472,628K free)
Used RAM: 2,864,761K (2,408,529K used pss +   456,232K kernel)
Lost RAM:   184,330K
    ZRAM:   174,628K physical used for   625,388K in swap (2,621,436K total swap)
Tuning: 256 (large 512), oom   322,560K, restore limit   107,520K (high-end-gfx)
复制代码

根据 dumpsys meminfo 的输出结果,可归结为以下表格:

划分类型 排序指标 含义
process PSS 以进程的PSS从大到小依次排序显示,每行显示一个进程,通常用来作初步的竞品分析
OOM adj PSS 展现当前系统内部运行的全部Android进程的内存状态和被杀顺序,越靠近下方的进程越容易被杀,排序按照一套复杂的算法,算法涵盖了先后台、服务或节目、可见与否、老化等
category PSS 以Dalvik/Native/.art mmap/.dex map等划分并按降序列出各种进程的总PSS分布状况
total - 总内存、剩余内存、可用内存、其余内存

此外,为了 查看单个 App 进程的内存信息,咱们能够输入以下命令:

dumpsys meminfo <pid> // 输出指定pid的某一进程
dumpsys meminfo --package <packagename> // 输出指定包名的进程,可能包含多个进程
复制代码

这里咱们输入 adb shell dumpsys meminfo 23104 这条命令,其中 23104 为 Awesome-WanAndroid App 的 pid,结果以下所示:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo 23104
Applications Memory Usage (in Kilobytes):
Uptime: 258375231 Realtime: 258375231

** MEMINFO in pid 23104 [json.chao.com.wanandroid] **
                Pss  Private  Private  SwapPss     Heap     Heap     Heap
                Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
Native Heap    46674    46620        0      164    80384    60559    19824
Dalvik Heap     6949     6912       16       23    12064     6032     6032
Dalvik Other     7672     7672        0        0
       Stack      108      108        0        0
      Ashmem      134      132        0        0
     Gfx dev    16036    16036        0        0
   Other dev       12        0       12        0
   .so mmap     3360      228     1084       27
  .jar mmap        8        8        0        0
  .apk mmap    28279    11328    11584        0
  .ttf mmap      295        0       80        0
  .dex mmap     7780       20     4908        0
  .oat mmap      660        0       92        0
  .art mmap     8509     8028      104       69
 Other mmap      982        8      848        0
 EGL mtrack    29388    29388        0        0
  GL mtrack    14864    14864        0        0
    Unknown     2532     2500        8       20
      TOTAL   174545   143852    18736      303    92448    66591    25856

App Summary
                   Pss(KB)
                    ------
       Java Heap:    15044
     Native Heap:    46620
            Code:    29332
           Stack:      108
        Graphics:    60288
   Private Other:    11196
          System:    11957

           TOTAL:   174545       TOTAL SWAP PSS:      303

Objects
           Views:      171         ViewRootImpl:        1
     AppContexts:        3           Activities:        1
          Assets:       18        AssetManagers:        6
   Local Binders:       32        Proxy Binders:       27
   Parcel memory:       11         Parcel count:       45
Death Recipients:        1      OpenSSL Sockets:        0
        WebViews:        0

SQL
        MEMORY_USED:      371
 PAGECACHE_OVERFLOW:       72          MALLOC_SIZE:      117

DATABASES
    pgsz     dbsz   Lookaside(b)          cache  Dbname
        4       60            109      151/32/18  /data/user/0/json.chao.com.wanandroid/databases/bugly_db_
        4       20             19         0/15/1  /data/user/0/json.chao.com.wanandroid/databases/aws_wan_android.db
复制代码

该命令输出了 进程的内存概要,咱们应该着重关注 四个要点,下面我将一一进行讲解。

一、查看 Native Heap 的 Heap Alloc 与 Dalvik Heap 的 Heap Alloc

  • 1)、Heap Alloc:表示 native 的内存占用,若是持续上升,则可能有泄漏
  • 2)、Heap Alloc:表示 Java 层的内存占用

二、查看 Views、Activities、AppContexts 数量变化状况

若是 Views 与 Activities、AppContexts 持续上升,则代表有内存泄漏的风险

三、SQL 的 MEMORY_USED 与 PAGECACHE_OVERFLOW

  • 1)、MEMOERY_USED:表示数据库使用的内存
  • 2)、PAGECACHE_OVERFLOW:表示溢出也使用的缓存,这个数值越小越好

四、查看 DATABASES 信息

  • 1)、pgsz:表示数据库分页大小,这里全是 4KB
  • 2)、Lookaside(b):表示使用了多少个 Lookaside 的 slots,可理解为内存占用的大小
  • 3)、cache:一栏中的 151/32/18 则分别表示 分页缓存命中次数/未命中次数/分页缓存个数,这里的未命中次数不该该大于命中次数

三、LeakInspector

LeakInspector 是腾讯内部的使用的 一站式内存泄漏解决方案,它是 Android 手机通过长期积累和提炼、集内存泄漏检测、自动修复系统Bug、自动回收已泄露Activity内资源、自动分析GC链、白名单过滤 等功能于一体,并 深度对接研发流程、自动分析责任人并提缺陷单的全链路体系

那么,LeakInspector 与 LeakCanary 又有什么不一样之处呢?

它们之间主要有 四个方面 的不一样,以下所示:

1、检测能力与原理方面不一样

一、检测能力

它们都支持对 Activity、Fragment 及其它自定义类的泄漏检测,可是,LeakInspector 还 增长了 Btiamp 的检测能力,以下所示:

  • 1)、检测有没有在 View 上 decode 超过该 View 尺寸的图片,如有则上报出现问题的 Activity 及与其对应的 View id,并记录它的个数与平均占用内存的大小。
  • 2)、检测图片尺寸是否超过全部手机屏幕大小,违规则报警。

这一个部分的实现原理,咱们能够采用 ARTHook 的方式来实现,还不清楚的朋友请再仔细看看大图检测的部分。

二、检测原理

两个工具的泄漏检测原理都是在 onDestroy 时检查弱引用,不一样之处在于 LeakInspector 直接使用 WeakReference 来检测对象是否已经被释放,而 LeakCanary 则使用 ReferenceQueue,二者效果是同样的。

而且针对 Activity,咱们一般都会使用 Application的 registerActivityLifecycleCallbacks 来注册 Activity 的生命周期,以重写 onActivityDestroyed 方法实现。可是在 Android 4.0 如下,系统并无提供这个方法,为了不手动在每个 Activity 的 onDestroy 中去添加这份代码,咱们能够使用 反射 Instrumentation 来截获 onDestory,以下降接入成本。代码以下所示:

Class<?> clazz = Class.forName("android.app.ActivityThread");
Method method = clazz.getDeclaredMethod("currentActivityThread", null);
method.setAccessible(true);
sCurrentActivityThread = method.invoke(null, null);
Field field = sCurrentActivityThread.getClass().getDeclaredField("mInstumentation");
field.setAccessible(true);
field.set(sCurrentActivityThread, new MonitorInstumentation());
复制代码

2、泄漏现场处理方面不一样

一、dump 采集

二者都能采集 dump,可是 LeakInspector 提供了回调方法,咱们能够增长更多的自定义信息,如运行时 Log、trace、dumpsys meminfo 等信息,以辅助分析定位问题。

二、白名单定义

这里的白名单是为了处理一些系统引发的泄漏问题,以及一些由于 业务逻辑要开后门的情形而设置 的。分析时若是碰到白名单上标识的类,则不对这个泄漏作后续的处理。两者的配置差别有以下两点:

  • 1)、LeakInspector 的白名单以 XML 配置的形式存放在服务器上。

    • 优势:跟产品甚至不一样版本的应用绑定,咱们能够很方便地修改相应的配置。
    • 缺点:白名单里的类不区分系统版本一刀切。
  • 1)、而LeakCanary的白名单是直接写死在其源码的AndroidExcludedRefs类里。

    • 优势:定义很是详细,并区分系统版本。
    • 缺点:每次修改一定得从新编译。
  • 2)、LeakCanary 的系统白名单里定义的类比 LeakInspector 中定义的多不少,由于它没有自动修复系统泄漏功能。

三、自动修复系统泄漏

针对系统泄漏,LeakInspector 经过 反射自动修复 了目前碰到的一些系统泄漏,只要在 onDestory 里面 调用 一个修复系统泄漏的方法便可。而 LeakCanary 虽然能识别系统泄漏,可是它仅仅对该类问题给出了分析,没有提供实际可用的解决方案。

四、回收资源(Activity内存泄漏兜底处理)

若是检测到发生了内存泄漏,LeakInspector 会对整个 Activity 的 View 进行遍历,把图片资源等一些占内存的数据释放掉,保证这次泄漏只会泄漏一个Activity的空壳,尽可能减小对内存的影响。代码大体以下所示:

if (View instanceof ImageView) {
    // ImageView ImageButton处理
    recycleImageView(app, (ImageView) view);
} else if (view instanceof TextView) {
    // 释放TextView、Button周边图片资源
    recycleTextView((TextView) view);
} else if (View instanceof ProgressBar) {
    recycleProgressBar((ProgressBar) view);
} else {
    if (view instancof android.widget.ListView) {
        recycleListView((android.widget.ListView) view);
    } else if (view instanceof android.support.v7.widget.RecyclerView) {
        recycleRecyclerView((android.support.v7.widget.RecyclerView) view);
    } else if (view instanceof FrameLayout) {
        recycleFrameLayout((FrameLayout) view);
    } else if (view instanceof LinearLayout) {
        recycleLinearLayout((LinearLayout) view);
    }
    
    if (view instanceof ViewGroup) {
        recycleViewGroup(app, (ViewGroup) view);
    }
}
复制代码

这里以 recycleTextView 为例,它回收资源的方式以下所示:

private static void recycleTextView(TextView tv) {
    Drawable[] ds = tv.getCompoundDrawables();
    for (Drawable d : ds) {
        if (d != null) {
            d.setCallback(null);
        }
    }
    tv.setCompoundDrawables(null, null, null, null);
    // 取消焦点,让Editor$Blink这个Runnable再也不被post,解决内存泄漏。
    tv.setCursorVisible(false);
}
复制代码

3、后期处理不一样

一、分析与展现

采集 dump 以后,LeakInspector 会上传 dump 文件,并* 调用 MAT 命令行来进行分析*,获得此次泄漏的 GC 链。而 LeakCanary 则用开源组件 HAHA 来分析获得一个 GC 链。可是 LeakCanary 获得的 GC 链包含被 hold 住的类对象,通常都不须要用 MAT 打开 Hporf 便可解决问题。而 LeakInpsector 获得的 GC 链只有类名,还须要 MAT 打开 Hprof 才能具体去定位问题,不是很方便。

二、后续跟进闭环

LeakInspector 在 dump 分析结束以后,会提交缺陷单,而且把缺陷单分配给对应类的负责人。若是发现重复的问题则更新旧单,同时具有从新打开单等状态转换逻辑。而 LeakCanary 仅会在通知栏提醒用户,须要用户本身记录该问题并作后续处理。

4、配合自动化测试方面不一样

LeakInspector 跟自动化测试能够无缝结合,当自动化脚本执行中发现内存泄漏,能够由它采集 dump 并发送到服务进行分析,最后提单,整个流程是不须要人力介入的。而 LeakCanary 则把分析结果经过通知栏告知用户,须要人工介入才能进入下一个流程。

四、JHat

JHat 是 Oracle 推出的一款 Hprof 分析软件,它和 MAT 并称为 Java 内存静态分析利器。不一样于 MAT 的单人界面式分析,jHat 使用多人界面式分析。它被 内置在 JDK 中,在命令行中输入 jhat 命令可查看有没有相应的命令。

quchao@quchaodeMacBook-Pro ~ % jhat
ERROR: No arguments supplied
Usage:  jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>

    -J<flag>          Pass <flag> directly to the runtime system. For
		    example, -J-mx512m to use a maximum heap size of 512MB
    -stack false:     Turn off tracking object allocation call stack.
    -refs false:      Turn off tracking of references to objects
    -port <port>:     Set the port for the HTTP server.  Defaults to 7000
    -exclude <file>:  Specify a file that lists data members that should
		    be excluded from the reachableFrom query.
    -baseline <file>: Specify a baseline object dump.  Objects in
		    both heap dumps with the same ID and same class will
		    be marked as not being "new".
    -debug <int>:     Set debug level.
		        0:  No debug output
		        1:  Debug hprof file parsing
		        2:  Debug hprof file parsing, no server
    -version          Report version number
    -h|-help          Print this help and exit
    <file>            The file to read

For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".
复制代码

出现如上输出,则代表存在 jhat 命令。它的使用很简单,直在命令行输入 jhat xxx.hprof 便可,以下所示:

quchao@quchaodeMacBook-Pro ~ % jhat Documents/heapdump/new-33.hprof
Snapshot read, resolving...
Resolving 408200 objects...
Chasing references, expect 81 dots.................................................................................
Eliminating duplicate references.................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
复制代码

jHat 的执行过程是解析 Hprof 文件,而后启动 httpsrv 服务,默认是在 7000 端口监听 Web 客户端连接,维护 Hprof 解析后的数据,以持续供给 Web 客户端进行查询操做

启动服务器后,咱们打开 入口地址 127.0.0.1:7000 便可查看 All Classes 界面,以下图所示:

image

jHat 还有两个比较重要的功能,分别以下所示:

一、统计表

打开 127.0.0.1:7000/histo/,统计表界面以下所示:

image

能够到,按 Total Size 降序 排列了全部的 Class,而且,咱们还能够查看到每个 Class 与之对应的实例数量。

二、OQL 查询

OQL 是一种模仿 SQL 语句的查询语句,一般用来查询某个类的实例数量,打开 127.0.0.1:7000/oql/ 并输入 java.lang.String 查询 String 实例的数量,结果以下图所示:

image

JHat 比 MAT 更加灵活,且符合大型团队安装简单、团队协做的需求。可是,并不适合中小型高效沟通型团队使用。

五、ART GC Log

GC Log 分为 Dalvik 和 ART 的 GC 日志,关于 Dalvik 的 GC 日志,咱们在前篇 Android性能优化以内存优化 中已经详细讲解过了,接下来咱们说说 ART 的 GC 日志

ART 的日志与 Dalvik 的日志差距很是大,除了格式不一样以外,打印的时间也不一样,并且,它只有在慢 GC 时才会打印出来。下面咱们看看这条 ART GC Log:

Explicit (full) concurrent mark sweep GC freed 104710 (7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free,25MB/38MB paused 1.230ms total 67.216ms
GC产生的缘由 GC类型 采集方法 释放的数量和占用的空间 释放的大对象数量和所占用的空间 堆中空闲空间的百分比和(对象的个数)/(堆的总空间) 暂停耗时

GC 产生的缘由

GC 产生的缘由有以下九种:

  • 1)、Concurrent、Alloc、Explicit 跟 Dalvik 的基本同样,这里就不重复介绍了。
  • 2)、NativeAlloc:Native 内存分配时,好比为 Bitmaps 或者 RenderScript 分配对象, 这会致使Native内存压力,从而触发GC
  • 3)、Background:后台 GC,触发是为了给后面的内存申请预留更多空间
  • 4)、CollectorTransition:由堆转换引发的回收,这是运行时切换 GC 而引发的。收集器转换包括将全部对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在如下状况下出现:在内存较小的设备上,App 将进程状态从可察觉的暂停状态变动为可察觉的非暂停状态(反之亦然)
  • 5)、HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,一般发生在当 App 已经移动到可察觉的暂停进程状态。这样作的主要缘由是减小了内存使用并对堆内存进行碎片整理
  • 6)、DisableMovingGc:不是真正的触发 GC 缘由,发生并发堆压缩时,因为使用了 GetPrimitiveArrayCritical,收集会被阻塞。通常状况下,强烈建议不要使用 GetPrimitiveArrayCritical
  • 7)、HeapTrim:不是触发GC缘由,可是请注意,收集会一直被阻塞,直到堆内存整理完毕

GC 类型

GC 类型有以下三种:

  • 1)、Full:与Dalvik的 FULL GC 差很少
  • 2)、Partial:跟 Dalvik 的局部 GC 差很少,策略时不包含 Zygote Heap
  • 3)、Sticky:另一种局部中的局部 GC,选择局部的策略是上次垃圾回收后新分配的对象

GC采集的方法

GC 采集的方法有以下四种:

  • 1)、mark sweep:先记录所有对象,而后从 GC ROOT 开始找出间接和直接的对象并标注。利用以前记录的所有对象和标注的对象对比,其他的对象就应该须要垃圾回收了
  • 2)、concurrent mark sweep:使用 mark sweep 采集器的并发 GC
  • 3)、mark compact:在标记存活对象的时候,全部的存活对象压缩到内存的一端,而另外一端能够更加高效地被回收
  • 4)、semispace:在作垃圾扫描的时候,把全部引用的对象从一个空间移到另一个空间,而后直接 GC 剩余在旧空间中的对象便可

经过 GC 日志,咱们能够知道 GC 的量和 它对卡顿的影响,也能够 初步定位一些如主动调用GC、可分配的内存不足、过多使用Weak Reference 等问题。

六、Chrome Devtool

对于 HTML5 页面而言,抓取 JavaScript 的内存须要使用 Chrome Devtools 来进行远程调试。方式有以下两种:

  • 1)、直接把 URL 抓取出来放到 Chrome 里访问。
  • 2)、用 Android H5 远程调试。

纯H5

一、手机安装 Chrome,打开 USB 调试模式,经过 USB 连上电脑,在 Chrome 里打开一个页面,好比百度页面。而后在 PC Chrome 地址栏里访问 Chrome://inspect,以下图所示:

image

二、最后,直接点击 Chrome 下面的 inspect 选项便可弹出开发者工具界面。以下图所示:

image

默认 Hybrid H5 调试

Android 4.4 及以上系统的原生浏览器就是 Chrome 浏览器,能够使用 Chrome Devtool 远程调试 WebView,前提是须要在 App 的代码里把调试开关打开,以下代码所示:

if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.KITKAT && 是debug模式) {
    WebView.setWebContentsDebuggingEnabled(ture);
}
复制代码

打开后的调试方法跟纯 H5 页面调试方法同样,直接在 App 中打开 H5 页面,再到 PC Chrome 的 inpsector 页面就能够看到调试目标页面。

这里总结一下 JS 中几种常见的内存问题点

  • 1)、closure 闭包函数
  • 2)、事件监听
  • 3)、变量做用域使用不当,全局变量的引用致使没法释放
  • 4)、DOM 节点的泄漏

若想更深刻地学习 Chrome 开发者工具的使用方法,请查看 《Chrome开发者工具中文手册》

8、内存问题总结

在咱们进行内存优化的过程当中,有许多内存问题均可以归结为一类问题,为了便于之后快速地解决相似的内存问题,我将它们归结成了如下的多个要点

一、内类是有危险的编码方式

说道内类就不得不提到 ”this$0“,它是一种奇特的内类成员,每一个类实例都具备一个 this$0,当它的内类须要访问它的成员时,内类就会持有外类的 this$0,经过 this$0 就能够访问外部类全部的成员。

解决方案是在 Activity 关闭,即触发 onDestory 时解除内类和外部的引用关系。

二、普通 Hanlder 内部类的问题

这也是一个 this$0 间接引用的问题,对于 Handler 的解决方案通常能够归结为以下三个步骤:

  • 1)、把内类声明成 static:用来断绝 this$0 的引用。由于 static 描述的内类从 Java 编译原理的角度看,”内类“与”外类“相互独立,互相都没有访问对方成员变量的能力
  • 二、使用 WeakReference 来引用外部类的实例
  • 三、在外部类(如 Activity)销毁的时候使用 removeCallbackAndMessages 来移除回调和消息

这里须要在使用过程当中注意对 WeakReference 进行判空

三、登陆界面的内存问题

若是在闪屏页跳转到登陆界面时没有调用 finish(),则会形成闪屏页的内存泄漏,在碰到这种”过渡界面“的状况时,须要注意不要产生这样的内存 Bug

四、使用系统服务时产生的内存问题

咱们一般都会使用 getSystemService 方法来获取系统服务,可是当在 Activity 中调用时,会默认把 Activity 的 Context 传给系统服务,在某些不肯定的状况下,某些系统服务内部会产生异常,从而 hold 住外界传入的 Context。

解决方案是 直接使用 Applicaiton 的 Context 去获取系统服务

五、把 WebView 类型的泄漏装进垃圾桶进程

咱们都知道,对应 WebView 来讲,其 网络延时、引擎 Session 管理、Cookies 管理、引擎内核线程、HTML5 调用系统声音、视频播放组件等产生的引用链条没法及时打断,形成的内存问题基本上能够用”无解“来形容。

解决方案是咱们能够 把 WebView 装入另外一个进程。 具体为在 AndroidManifes 中对当前的 Activity 设置 android:process 属性便可,最后,在 Activity 的 onDestory 中退出进程,这样便可基本上终结 WebView 形成的泄漏

六、在适当的时候对组件进行注销

咱们在日常开发过程当中常常须要在Activity建立的时候去注册一些组件,如广播、定时器、事件总线等等。这个时候咱们应该在适当的时候对组件进行注销,如 onPause 或 onDestory 方法中

七、Handler / FrameLayout 的 postDelyed 方法触发的内存问题

不只在使用 Handler 的 sendMessage 方法时,咱们须要在 onDestory 中使用 removeCallbackAndMessage 移除回调和消息,在使用到 Handler / FrameLayout 的 postDelyed 方法时,咱们须要调用 removeCallbacks 去移除实现控件内部的延时器对 Runnable 内类的持有

八、图片放错资源目录也会有内存问题

在作资源适配的时候,由于须要考虑到 APK 的瘦身问题,没法为每张图片在每一个 drawable / mipmap 目录下安置一张适配图片的副本。不少同窗不知道图片应该放哪一个目录,若是放到分辨率低的目录如 hdpi 目录,则可能会形成内存问题,这个时候建议尽可能问设计人员要高品质图片而后往高密度目录下方,如 xxhdpi 目录,这样 在低密屏上”放大倍数“是小于1的,在保证画质的前提下,内存也是可控的。也能够使用 Drawable.createFromSream 替换 getResources().getDrawable 来加载,这样即可以绕过 Android 的默认适配规则

对于已经被用户使用物理“返回键”退回到后台的进程,若是包含了如下 两点,则 不会被轻易杀死

  • 1)、进程包含了服务 startService,而服务自己调用了 startForeground(低版本需经过反射调用)
  • 2)、主 Activity 没有实现 onSaveInstanceState 接口

但建议 在运行一段时间(如3小时)后主动保存界面进程(位于后台),而后重启它,这样能够有效地下降内存负载

九、列表 item 被回收时注意释放图片的引用

咱们应该在 item 被回收不可见时去释放掉对图片的引用。若是你使用的是 ListView,因为每次 item 被回收后被再次利用都会去从新绑定数据,因此只需在 ImageView 回调其 onDetchFromWindow 方法的时候区释放掉图片的引用便可。若是你使用的是 RecyclerView,由于被回收不可见时第一次选择是放进 mCacheView中,可是这里面的 item 被复用时并不会去执行 bindViewHolder 来从新绑定数据,只有被回收进 mRecyclePool 后拿出来复用才会从新绑定数据。因此此时咱们应该在 item 被回收进 RecyclePool 的时候去释放图片的引用,这里咱们只要去 重写 Adapter 中的 onViewRecycled 方法 就能够了,代码以下所示:

@Override
public void onViewRecycled(@Nullable VH holder) {
    super.onViewRecycled(holder);
    if (holder != null) {
        //作释放图片引用的操做
    }
}
复制代码

十、使用 ViewStub 进行占位

咱们应该使用 ViewStub 对那些没有立刻用到的资源去作延迟加载,而且还有不少大几率不会出现的 View 更要去作懒加载,这样能够等到要使用时再去为它们分配相应的内存。

十一、注意定时清理 App 过期的埋点数据

产品或者运营为了统计数据会在每一个版本中不断地增长新的埋点。因此咱们须要按期地去清理一些过期的埋点,以此来 适当地优化内存以及CPU的压力

十二、针对匿名内部类 Runnable 形成内存泄漏的处理

咱们在作子线程操做的时候,喜欢使用匿名内部类 Runnable 来操做。可是,若是某个 Activity 放在线程池中的任务不能及时执行完毕,在 Activity 销毁时很容易致使内存泄漏。由于这个匿名内部类 Runnable 类持有一个指向 Outer 类的引用,这样一来若是 Activity 里面的 Runnable 不能及时执行,就会使它外围的 Activity 没法释放,产生内存泄漏。从上面的分析可知,只要在 Activity 退出时没有这个引用便可,那咱们就经过反射,在 Runnable 进入线程池前先干掉它,代码以下所示:

Field f = job.getClass().getDeclaredField("this$0");
f.setAccessible(true);
f.set(job, null);
复制代码

这个任务就是咱们的 Runnable 对象,而 ”this$0“ 就是上面所指的外部类的引用了。这里注意使用 WeakReference 装起来,要执行了先 get 一下,若是是 null 则说明 Activity 已经回收,任务就放弃执行。

9、内存优化常见问题

一、大家内存优化项目的过程是怎么作的?

一、分析现状、确认问题

咱们发现咱们的 APP 在内存方面可能存在很大的问题,第一方面的缘由是咱们的线上的 OOM 率比较高。

第二点呢,咱们常常会看到在咱们的 Android Studio 的 Profiler 工具中内存的抖动比较频繁。

这是咱们一个初步的现状,而后在咱们知道了这个初步的现状以后,进行了问题的确认,咱们通过一系列的调研以及深刻研究,咱们最终发现咱们的项目中存在如下几点大问题,好比说:内存抖动、内存溢出、内存泄漏,还有咱们的Bitmap 使用很是粗犷

二、针对性优化

好比 内存抖动的解决 => Memory Profiler 工具的使用(呈现了锯齿张图形) => 分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),也能够说说 内存泄漏或内存溢出的解决

三、效率提高

为了避免增长业务同窗的工做量,咱们使用了一些工具类或 ARTHook 这样的 大图检测方案,没有任何的侵入性。同时,咱们将这些技术教给了你们,而后让你们一块儿进行 工做效率上的提高

咱们对内存优化工具Profiler Memory、MAT 的使用比较熟悉,所以 针对一系列不一样问题的状况,咱们写了 一系列解决方案的文档,分享给你们。这样,咱们 整个团队成员的内存优化意识就变强 了。

二、你作了内存优化最大的感觉是什么?

一、磨刀不误砍柴工

咱们一开始并无直接去分析项目中代码哪些地方存在内存问题,而是先去学习了 Google 官方的一些文档,好比说学习了 Memory Profiler 工具的使用、学习了 MAT 工具的使用,在咱们将这些工具学习熟练以后,当在咱们的项目中遇到内存问题时,咱们就可以很快地进行排查定位问题进行解决。

二、技术优化必须结合业务代码

一开始,咱们作了总体 APP 运行阶段的一个内存上报,而后,咱们在一些重点的内存消耗模块进行了一些监控,可是,后面发现这些监控并无紧密地结合咱们的业务代码,好比说在梳理完项目以后,发现咱们项目中存在使用多个图片库的状况,多个图片库的内存缓存确定是不公用的,因此 致使咱们整个项目的内存使用量很是高。因此进行技术优化时必须结合咱们的业务代码。

三、系统化完善解决方案

咱们在作内存优化的过程当中,不只作了 Android 端的优化工做,还将咱们 Android 端一些数据的采集上报到了咱们的服务器,而后传到咱们的 APM 后台,这样,方便咱们的不管是 Bug 跟踪人员或者是 Crash 跟踪人员进行一系列问题的解决。

三、如何检测全部不合理的地方?

好比说 大图片的检测,咱们最初的一个方案是经过继承 ImageView重写 它的 onDraw 方法来实现。可是,咱们在推广它的过程当中,发现不少开发人员并不接受,由于不少 ImageView 以前已经写过了,你如今让他去替换,工做成本是比较高的。因此说,后来咱们就想,有没有一种方案能够 免替换,最终咱们就找到了 ARTHook 这样一个 Hook 的方案。

10、总结

对于 内存优化的专项优化 而言,咱们要着重注意两点,即 优化大方向 和 优化细节

一、优化大方向

对于 优化的大方向,咱们应该 优先去作见效快的地方,主要有如下三部分:

  • 1)、内存泄漏
  • 2)、内存抖动
  • 3)、Bitmap

二、优化细节

对于 优化细节,咱们应该 注意一些系统属性或内存回调的使用 等等,主要能够细分为以下六部分:

  • 1)、LargeHeap 属性
  • 2)、onTrimMemory / onLowMemory
  • 3)、使用优化事后的集合:如 SparseArray 类簇
  • 4)、谨慎使用 SharedPreference
  • 5)、谨慎使用外部库
  • 6)、业务架构设计合理

三、内存优化体系化建设总结

在这篇文章中,咱们除了创建了 内存的监控闭环 这一核心体系以外,还实现了如下 十大组件 / 策略

  • 1)、根据设备分级来使用不一样的内存和分配回收策略
  • 2)、针对低端机作了功能或图片加载格式的降级处理
  • 3)、针对缓存滥用的问题实现了统一的缓存管理组件
  • 4)、实现了大图监控和重复图片的监控
  • 5)、在前台每隔必定时间去获取当前应用内存占最大内存的比例,当超过设定阈值时则主动释放应用 cache
  • 6)、当 UI 隐藏时释放内存以增长系统缓存应用进程的能力
  • 7)、高效实现了应用全局内的 Bitmap 监控
  • 8)、实现了全局的线程监控
  • 9)、针对内存使用的重度场景实现了 GC 监控
  • 10)、实现了线下的 native 内存泄漏监控

最后,当监控到 应用内存超过阈值时,还定制了 完善的兜底策略重启应用进程

总的来看,要创建一套 全面且成体系的内存优化及监控 是很是重要也是极具挑战性的一项工做。而且,目前各大公司的 内存优化体系 也正处于 不断演进的历程 之中,其目的不外乎:实现更健全的功能、更深层次的定位问题、快速准确地发现线上问题

路漫漫其修远兮,吾将上下而求索

参考连接:

一、国内Top团队大牛带你玩转Android性能分析与优化 第四章 内存优化

二、极客时间之Android开发高手课 内存优化

三、微信 Android 终端内存优化实践

四、GMTC-Android内存泄漏自动化链路分析组件Probe.key

五、Manage your app's memory

六、Overview of memory management

七、Android内存优化杂谈

八、Android性能优化以内存篇

九、管理应用的内存

十、《Android移动性能实战》第二章 内存

十一、天天一个linux命令(44):top命令

十二、Android内存分析命令

Contanct Me

● 微信:

欢迎关注个人微信:bcce5360

● 微信群:

微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。

相关文章
相关标签/搜索