写点什么

深入探索 Android 内存优化(炼狱级别 - 上),安卓消息分发机制

用户头像
Android架构
关注
发布于: 2021 年 11 月 06 日

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


本篇文章非常长,建议收藏后慢慢享用~


目录

  • 一、[重识内存优化](


)


  • 1、手机 RAM

  • 2、内存优化的纬度

  • 3、内存问题

  • 二、[常见工具选择](


)


  • 1、Memory Profiler

  • 2、Memory Analyzer

  • 3、LeakCanary

  • 三、[Android 内存管理机制回顾](


)


  • 1、Java 内存分配

  • 2、Java 内存回收算法

  • 3、Android 内存管理机制

  • 4、小结

  • 四、[内存抖动](


)


  • 1、那么,为什么内存抖动会导致 OOM?

  • 2、内存抖动解决实战

  • 3、内存抖动常见案例

  • 五、[内存优化体系化搭建](


)


  • 1、MAT 回顾

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

  • 3、建立线上应用内存监控体系

  • 4、建立全局的线程监控组件

  • 5、GC 监控组件搭建

  • 6、建立线上 OOM 监控组件:Probe

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

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

  • 9、设置内存兜底策略

  • 10、更深入的内存优化策略

  • 六、[内存优化演进](


)


  • 1、自动化测试阶段

  • 2、LeakCanary

  • 3、使用基于 LeakCannary 的改进版 ResourceCanary

  • 七、[内存优化工具](


)


  • 1、top

  • 2、dumpsys meminfo

  • 3、LeakInspector

  • 4、JHat

  • 5、ART GC Log

  • 6、Chrome Devtool

  • 八、[内存问题总结](


)


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

  • 2、普通 Hanlder 内部类的问题

  • 3、登录界面的内存问题

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

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

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

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

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

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

  • 10、使用 ViewStub 进行占位

  • 11、注意定时清理 App 过时的埋点数据

  • 12、针对匿名内部类 Runnable 造成内存泄漏的处理

  • 九、[内存优化常见问题](


)


  • 1、你们内存优化项目的过程是怎么做的?

  • 2、你做了内存优化最大的感受是什么?

  • 3、如何检测所有不合理的地方?

  • 十、[总结](


)


  • 1、优化大方向

  • 2、优化细节

  • 3、内存优化体系化建设总结

一、重识内存优化

Android 给每个应用进程分配的内存都是非常有限的,那么,为什么不能把图片下载下来都放到磁盘中呢?那是因为放在 内存 中,展示会更 “”,快的原因有两点,如下所示:


  • 1)、硬件快:内存本身读取、存入速度快。

  • 2)、复用快:解码成果有效保存,复用时,直接使用解码后对象,而不是再做一次图像解码。


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


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

1、手机 RAM

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


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

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

当系统 内存充足 的时候,我们可以 多用 一些获得 更好的性能。当系统 内存不足 的时候,我们希望可以做到 ”用时分配,及时释放“。

2、内存优化的纬度

对于 Android 内存优化来说又可以细分为如下两个维度,如下所示:


  • 1)、RAM 优化

  • 2)、ROM 优化

1、RAM 优化

主要是 降低运行时内存。它的 目的 有如下三个:


  • 1)、防止应用发生 OOM

  • 2)、降低应用由于内存过大被 LMK 机制杀死的概率

  • 3)、避免不合理使用内存导致 GC 次数增多,从而导致应用发生卡顿

2、ROM 优化

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

3、内存问题

那么,内存问题主要是有哪几类呢?内存问题通常来说,可以细分为如下 三类:


  • 1)、内存抖动

  • 2)、内存泄漏

  • 3)、内存溢出


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

1、内存抖动

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


这个问题在 Dalvik 虚拟机 上会 更加明显,而 ART 虚拟机内存管理跟回收策略 上都做了 大量优化内存分配和 GC 效率相比提升了 5~10 倍,所以 出现内存抖动的概率会小很多

2、内存泄漏

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

3、内存溢出

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


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

二、常见工具选择

在 [Android 性能优化之内存优化](


) 中我们已经介绍过了相关的优化工具,这里再简单回顾一下。

1、Memory Profiler

作用

  • 1)、实时图表展示应用内存使用量

  • 2)、用于识别内存泄漏、抖动等

  • 3)、提供捕获堆转储、强制 GC 以及根据内存分配的能力

优点

  • 1)、方便直观

  • 2)、线下使用

2、Memory Analyzer

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

3、LeakCanary

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


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


  • 1)、虽然使用了 idleHandler 与多进程,但是 dumphprof 的 SuspendAll Thread 的特性依然会导致应用卡顿

  • 2)、在三星等手机,系统会缓存最后一个 Activity,此时应该采用更严格的检测模式

三、Android 内存管理机制回顾

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

1、Java 内存分配

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


  • 1)、方法区:主要存放静态常量

  • 2)、虚拟机栈:Java 变量引用

  • 3)、本地方法栈:native 变量引用

  • 4)、堆:对象

  • 5)、程序计数器:计算当前线程的当前方法执行到多少行

2、Java 内存回收算法

1、标记-清除算法

流程可简述为 两步


  • 1)、标记所有需要回收的对象

  • 2)、统一回收所有被标记的对象

优点

实现比较简单。

缺点

  • 1)、标记、清除效率不高

  • 2)、产生大量内存碎片

2、复制算法

流程可简述为 三步


  • 1)、将内存划分为大小相等的两块

  • 2)、一块内存用完之后复制存活对象到另一块

  • 3)、清理另一块内存

优点

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

缺点

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

3、标记-整理算法

流程可简述为 三步


  • 1)、标记过程与 标记-清除算法 一样

  • 2)、存活对象往一端进行移动

  • 3)、清理其余内存

优点

  • 1)、避免 标记-清除 导致的内存碎片

  • 2)、避免复制算法的空间浪费

4、分代收集算法

现在 主流的虚拟机 一般用的比较多的还是分代收集算法,它具有如下 特点


  • 1)、结合多种算法优势

  • 2)、新生代对象存活率低,使用 复制算法

  • 3)、老年代对象存活率高,使用 标记-整理算法

3、Android 内存管理机制

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


对于 OOM 场景 其实可以细分为如下两种:


  • 1)、内存真正不足

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


我们需要着重注意一下这两种的区分。

4、小结

以 Android 中虚拟机的角度来说,我们要清楚 Dalvik 与 ART 区别Dalvik 仅固定一种回收算法,而 ART 回收算法可在 运行期按需选择,并且,ART 具备 *内存整理


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


  • 能力,减少内存空洞


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

四、内存抖动

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


并且,它的危害也很严重,通常会导致 页面卡顿,甚至造成 OOM

1、那么,为什么内存抖动会导致 OOM?

主要原因有如下两点:


  • 1)、频繁创建对象,导致内存不足及碎片(不连续)

  • 2)、不连续的内存片无法被分配,导致 OOM

2、内存抖动解决实战

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


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


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

3、内存抖动常见案例

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

1、字符串使用加号拼接

  • 1)、使用 StringBuilder 替代

  • 2)、初始化时设置容量,减少 StringBuilder 的扩容

2、资源复用

  • 1)、使用 全局缓存池,以 重用频繁申请和释放的对象

  • 2)、注意 结束 使用后,需要 手动释放对象池中的对象

3、减少不合理的对象创建

  • 1)、ondraw、getView 中创建的对象尽量进行复用

  • 2)、避免在循环中不断创建局部变量

4、使用合理的数据结构

使用 SparseArray 类族、ArrayMap 来替代 HashMap

五、内存优化体系化搭建

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


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


而对于 内存泄漏的分析 一般可简述为如下 两步


  • 1)、使用 Memory Profiler 初步观察

  • 2)、通过 Memory Analyzer 结合代码确认

1、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


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

1、Dominator(支配者):

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

2、Histogram 和 dominator_tree 的区别:

  • 1)、Histogram 显示 Shallow Heap、Retained Heap、Objects,而 dominator_tree 显示的是 Shallow Heap、Retained Heap、Percentage

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

3、thread_overview

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

4、Top Consumers

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


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

5、Leak Suspects

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

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

在介绍图片监控体系的搭建之前,首先我们来回顾下 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 一般限制为 256、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 以下所占的比例


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

1、统一图片库

在项目中,我们需要 收拢图片的调用,避免使用 Bitmap.createBitmap、BitmapFactory 相关的接口创建 Bitmap,而应该使用自己的图片框架

2、设备分级优化策略

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


因此,我们可以使用类似 [device-year-class](


) 的策略对设备进行分级,对于低端机用户可以关闭复杂的动画或”重功能“,使用 565 格式的图片或更小的缓存内存 等等。


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

3、建立统一的缓存管理组件

**建立统一的缓存管理组件(参考 [ACache](


)),并合理使用 OnTrimMemory / LowMemory 回调,根据系统不同的状态去释放相应的缓存与内存**。


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


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

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

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


针对低端机用户可以推出 4MB 的轻量级版本,如今日头条极速版、Facebook Lite。

5、线下大图片检测

在开发过程中,如果检测到不合规的图片使用(如图片宽度超过 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 通常的使用步骤为如下三个步骤:


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


compile 'me.weishu:epic:0.6.0'


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


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


@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);Thread t = (Thread) param.thisObject;Log.i(TAG, "thread:" + t + ", exit..");}}


3、注入 Hook 好的方法:


DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());


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

项目实战

以 [Awesome-WanAndroid](


) 项目为例,首先,在 WanAndroidApp 的 onCreate 方法中添加如下代码:


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


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


接下来,我们来实现我们的 ImageHook 类,代码如下所示:


public class ImageHook extends XC_MethodHook {


@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);// 1ImageView 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) {// 2、图标宽高都大于 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 {// 3、当宽高度等于 0 时,说明 ImageView 还没有进行绘制,使用 ViewTreeObserver 进行大图检测的处理。final Throwable stackTrace = new RuntimeException();view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {@Overridepublic 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)、兼容性问题大,开源方案不能带到线上环境

6、线下重复图片检测

首先我们来了解一下这里的 重复图片 所指的概念: 即 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 获取整个引用链信息并打印

7、建立全局的线上 Bitmap 监控

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


  • 1)、创建场景比较单一:在 Java 层调用 Bitmap.create 或 BitmapFactory 等方法创建,可以封装一层对 Bitmap 创建的接口,注意要 包含调用第三方库产生的 Bitmap,这里我们具体可以使用 ASM 编译插桩 + Gradle Transform 的方式来高效地实现。

  • 2)、创建频率比较低

  • 3)、和 Java 对象的生命周期一样服从 GC,可以使用 WeakReference 来追踪 Bitmap 的销毁

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
深入探索 Android 内存优化(炼狱级别-上),安卓消息分发机制