深刻探索 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 编译插桩 + Gradle Transform 的方式来高效地实现。
  • 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 => 系统问题修复

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

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

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

参考连接:

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

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

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

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

五、Manage your app's memory

六、Overview of memory management

七、Android内存优化杂谈

八、Android性能优化以内存篇

九、管理应用的内存

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

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

十二、Android内存分析命令

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

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

相关文章
相关标签/搜索