深入理解图片内存优化的常见方案和 AndroidBitmapMonitor 的原理

image.png

大家好,我是 shixin。

通过上一篇文章 《自研的内存分析利器开源了!Android Bitmap Monitor 助你定位不合理的图片使用》 我们知道了好用的图片内存分析工具 AndroidBitmapMonitor,现在我们来了解下它的原理。

这篇文章主要包括三部分:

  1. 为什么要关注图片占用的内存
  • 很多人可能会觉得,都已经 2023 年了,大多数设备及 APP 都是 64 位的,为什么还需要关注内存?这一部分做一个简单的解答
  1. 图片内存监控、分析常见方案
  • 这一部分我们来探讨下目前社区里常用的一些图片内存监控方案,及优缺点。
  1. 介绍新方案的功能和原理

image.png

为什么要关注图片占用的内存

之所以要关注图片内存,是因为内存是 Android 性能优化的重要指标,而图片通常是 app 内存使用的大头。

image.png

通常来说,内存使用不当会有这些问题:

  1. 崩溃
  2. 后台存活时间短
  3. 卡顿

崩溃是指虚拟内存不足导致的应用 crash,包括 Java 内存不足、Native 内存不足等原因。很多同学做内存优化时往往只关注 Java 内存,但随着 Android 官方对系统的优化(比如在 8.0 以后将图片数据保存在 Native 内存中)和内存泄漏排查工具的完善,app 的Java 内存问题越来越少, 遗留的棘手问题常常是 Native 内存问题。

后台存活时间短是指在内存不足时的 low memory killer 机制会根据进程优先级和内存使用情况强制关闭进程。如果应用在后台、且内存使用较高,很容易被系统强制关闭。

卡顿是指内存抖动引发的频繁垃圾回收,垃圾回收一方面会抢占主线程和渲染线程的 CPU 、另一方面也可能会触发阻塞式 GC 直接导致卡顿。

image.png

这几点是优化内存的必要性。而图片由于其动辄占用几 MB 的内存,经常成为内存过高的元凶。比如在分辨率为 3200 x 1440 的手机上,一张撑满全屏的图片就要占用 17Mb(3200 x 1440 x 4)。

图片使用的内存如此之大,导致线上常常会出现这种问题:

  1. 服务端下发的图片尺寸比实际要展示的大太多,导致内存使用过多甚至崩溃
  2. Bitmap 创建后没有及时回收,导致反复进入退出页面后内存不断上涨
  3. 快速滑动列表时一下子加载过多图片,导致内存飙升、卡顿

随着 app 的复杂度提升,这些问题出现的可能性越来越高。因此我们有必要关注图片内存情况,掌握有效的监控、分析手段,从而在 app 遇到图片内存时能够及时的解决。

好了,这一节我们了解了为什么要关注图片占用的内存,下一节来看下常见的图片内存分析方案。

图片内存分析常见方案

图片内存分析,是指获取到 app 在某个时间段内创建的图片总数、占用内存大小和创建堆栈,从而定位到导致内存异常的代码。

目前常见的图片内存分析方案有这几种:

image.png

如上图所示,主要有 HPROF 分析、Java Hook 和编译时修改字节码三种方式。

HPROF 分析

image.png

我们在开发期间或者复现问题时,可以通过 hprof dump 的方式获取 Java 对象的堆快照,从而找到其中的 Bitmap 对象。

因为 Android 中图片要加载出来最终需要创建 Bitmap 对象,所以通过 Java 的 Bitmap 对象的长宽我们就可以估算出图片的大概尺寸。

这种方式的优点是简单方便,通过 Android Studio 或者 MAT 就可以完成;但缺点是只能用于 debug 包,另外常常有很多 Bitmap 对象的引用链是通用的路径,导致无法定位到导致问题的代码(Android Studio 的 Bitmap Preview 功能只能支持 8.0 以下系统)。

Java Hook

image.png

Java hook 是指通过 YAHFA、epic 等框架,在运行时替换图片创建的相关代码入口函数,从而实现拦截 Bitmap 创建。

以 YAHFA 为例,要拦截 ImageView.setImageBitmap 函数,可以创建一个这样的代理类:

image.png

在上面的代码中,通过 className methodName 和 methodSig 可以声明要拦截的具体方法,然后在 hook 函数中,可以执行我们的拦截逻辑,比如记录 Bitmap 的尺寸信息。

image.png

这种方式的优点是实现简单,可以拿到的信息较多;缺点是不够稳定,因为底层原理是替换 ArtMethod 的 entryPoint(入口点),由于不同 Android 版本上 ArtMethod 中的结构有变化,因此寻找 entryPoint 的过程需要兼容不同版本,容易出现兼容性问题,只能在线下使用。

编译时修改字节码

image.png

在线上要监控图片内存,常用的方式是在编译时通过 AspectJ/ASM/JavaAssit 等方式拦截 Bitmap 创建的代码,在其中统计 Bitmap 的长宽、创建堆栈等信息。

image.png

和 JavaHook 不同的在于,编译时修改字节码是修改 APP 中的代码,而不是修改系统的代码,因此稳定性得到了保障。

这种方式的优点是可以获取到比较全面的信息;缺点是需要拦截的代码比较多,需要兼容不同版本的 API,成本较高,同时获取到的堆栈常常是图片加载库的堆栈,无法直接定位到业务代码。

下面是图片创建相关的 API,可以看到涉及的方法很多:

image.png

这一节我们了解了常见的图片内存分析方案的优缺点和使用场景。

接下来我们来一看一种更加完善的新方案 Android Bitmap Monitor。

新方案是什么样的和实现原理

image.png

Android Bitmap Monitor 就是今天要介绍的新方案,这一节我们来看下它的功能和实现原理。

Android Bitmap Monitor 是一个开源的 Android 图片内存分析工具,可以帮助开发者快速发现应用内加载的图片是否合理,比如占用内存大小是否合适、是否存在泄漏、缓存是否及时清理、是否加载了当前并不需要的图片等等。

https://github.com/shixinzhang/AndroidBitmapMonitor

支持这些功能:

  1. 支持获取内存中的图片数量及占用内存
  2. 提供 Bitmap 创建堆栈及线程
  3. 支持导出 Bitmap 图片,在堆栈无法看出问题时可以用来定位图片所属业务
  4. 支持动态开关,可以统计具体业务的数据

接下来我们来看下它的三个核心功能的实现原理:

image.png

获取内存中的图片宽高和堆栈

image.png

首先,AndroidBitmapMonitor 通过 inline-hook 的方式拦截了 Java Bitmap 对象创建的统一入口,这就避免了前面提到的了运行时 epic hook 和编译时 AOP 拦截的问题–需要兼容不同的图片创建代码。它拦截的哪个入口呢?这需要我们了解下不同版本的 Bitmap 创建流程。

我们知道,为了减少图片内存对应用稳定性的影响,Android 官方对图片的像素数据保存方式做了多次修改,目前的情况是:

  1. Android 8.0 以前及 Android 3.0 以后,像素数据保存在 Java 堆内存中
  2. Android 8.0 开始,像素数据保存在 Native 内存中

这样修改的结果就是,Java 层 Bitmap 对象只保存了长宽和是否回收的信息,没有保存像素数据,因此通过 Bitmap 对象无法获取到图片的真实数据,这也是前面提到的几种方案的统一问题。

但是,不论上层是通过什么方式创建的图片,最终都会执行到 Native 层的 Bitmap.cpp 的 Bitmap_creator 函数,在其中创建 Java 层的 Bitmap 对象并保存像素数据。

因此,我们可以通过 hook 这个函数,就可以拦截到图片创建的信息,比如宽高和堆栈信息。

对应的代码位置:https://github.com/shixinzhang/AndroidBitmapMonitor/blob/master/library/src/main/cpp/bitmap_monitor.cpp#L352

统计没有被回收的图片

知道了创建的图片信息是第一步,更重要的是知道哪些图片没有被及时回收。

经常遇到的图片泄漏问题:手动 decode 的 Bitmap 没有及时调用 recycle,导致反复进入页面内存不停上涨,最终导致功能异常。

在 Android 不同版本上,Bitmap 对象的释放流程有所不同:

  1. Android 8.0 及以前版本:调用 Java Bitmap 的 recycle 方法通过 JNI 调用只是释放了引用,图片的像素数据所占内存需要等待 GC 执行时才释放
  2. 从 8.0 开始,会直接释放掉 native 内存

image.png

两者的共同点是在执行后 Java Bitmap 的mRecycled 状态会变为 true。因此我们可以通过轮训 Bitmap 对象的 mRecycled 属性来判断这个图片是否被回收,实现方式如下图所示:

image.png

通过前面的图片创建流程监控我们拿到了当前创建的所有图片数据,然后可以通过一个线程定时轮训当前拿到的图片对象状态,当发现图片引用被回收或图片对象的 mRecycled 为 true 时,从记录中移除这个图片数据,最后得到的就是没有被回收的图片。

对应的代码位置:https://github.com/shixinzhang/AndroidBitmapMonitor/blob/master/library/src/main/cpp/bitmap_monitor.cpp#L240

图片还原

image.png

上一节对比不同方案时,我们提到有时候图片创建是通过图片库统一完成的,这种情况下获取到的堆栈无法看出业务代码。

这种情况下我们就需要通过图片内容来判断到底是哪里的业务有问题。

可能有小伙伴知道,Android Studio 的 Bitmap Preview 功能是支持查看图片内容的,但很可惜只支持 Android 8.0 以前的设备。这是因为它的实现原理是通过 HPROF 中 Bitmap 对象的 mBuffer 数据,因此只支持 8.0 以前的手机。

AndroidBitmapMonitor 实现了全版本的图片还原功能,根本区别就在于,是从 Native 层做的像素数据获取。

NDK 的 bitmap.h 提供了 AndroidBitmap_lockPixels 函数,通过它我们可以获取到图片的像素数据:
image.png

我们知道,图片本质上就是像素点的集合:

image.png

遍历像素数据,然后按照通用图片的格式(比如 BMP、PNG)输出为文件,就可以获取到图片的完整内容。

对应的代码位置:
https://github.com/shixinzhang/AndroidBitmapMonitor/blob/master/library/src/main/cpp/bitmap_monitor.cpp#L40

总结

image.png

好了,到这里我们就了解了图片内存分析新方案 AndroidBitmapMonitor 的实现原理。

AndroidBitmapMonitor 可以为我们提供详细的图片创建信息,基于它可以实现的功能有这些:

  1. 大图报警
  2. 图片泄漏监控
  3. 图片重复解码等等

源码地址:https://github.com/shixinzhang/AndroidBitmapMonitor


好了,这篇文章到这里就结束了,感谢你的阅读,愿你平安顺遂。

如果对你有帮助,欢迎评论点赞转发,你的支持是我最大的动力❤️

推荐阅读:

两年创业的得与失

简历怎么投效率最高

七年老安卓的九十月小结

六年安卓开发的技术回顾和展望

两位阿里 P10 的成长经历,让我学到这几点

猜你喜欢

转载自blog.csdn.net/u011240877/article/details/129782407