大家好,我是 shixin。
通过上一篇文章 《自研的内存分析利器开源了!Android Bitmap Monitor 助你定位不合理的图片使用》 我们知道了好用的图片内存分析工具 AndroidBitmapMonitor,现在我们来了解下它的原理。
这篇文章主要包括三部分:
之所以要关注图片内存,是因为内存是 Android 性能优化的重要指标,而图片通常是 app 内存使用的大头。
通常来说,内存使用不当会有这些问题:
崩溃是指虚拟内存不足导致的应用 crash,包括 Java 内存不足、Native 内存不足等原因。很多同学做内存优化时往往只关注 Java 内存,但随着 Android 官方对系统的优化(比如在 8.0 以后将图片数据保存在 Native 内存中)和内存泄漏排查工具的完善,app 的Java 内存问题越来越少, 遗留的棘手问题常常是 Native 内存问题。
后台存活时间短是指在内存不足时的 low memory killer 机制会根据进程优先级和内存使用情况强制关闭进程。如果应用在后台、且内存使用较高,很容易被系统强制关闭。
卡顿是指内存抖动引发的频繁垃圾回收,垃圾回收一方面会抢占主线程和渲染线程的 CPU 、另一方面也可能会触发阻塞式 GC 直接导致卡顿。
这几点是优化内存的必要性。而图片由于其动辄占用几 MB 的内存,经常成为内存过高的元凶。比如在分辨率为 3200 x 1440 的手机上,一张撑满全屏的图片就要占用 17Mb(3200 x 1440 x 4)。
图片使用的内存如此之大,导致线上常常会出现这种问题:
随着 app 的复杂度提升,这些问题出现的可能性越来越高。因此我们有必要关注图片内存情况,掌握有效的监控、分析手段,从而在 app 遇到图片内存时能够及时的解决。
好了,这一节我们了解了为什么要关注图片占用的内存,下一节来看下常见的图片内存分析方案。
图片内存分析,是指获取到 app 在某个时间段内创建的图片总数、占用内存大小和创建堆栈,从而定位到导致内存异常的代码。
目前常见的图片内存分析方案有这几种:
如上图所示,主要有 HPROF 分析、Java Hook 和编译时修改字节码三种方式。
我们在开发期间或者复现问题时,可以通过 hprof dump 的方式获取 Java 对象的堆快照,从而找到其中的 Bitmap 对象。
因为 Android 中图片要加载出来最终需要创建 Bitmap 对象,所以通过 Java 的 Bitmap 对象的长宽我们就可以估算出图片的大概尺寸。
这种方式的优点是简单方便,通过 Android Studio 或者 MAT 就可以完成;但缺点是只能用于 debug 包,另外常常有很多 Bitmap 对象的引用链是通用的路径,导致无法定位到导致问题的代码(Android Studio 的 Bitmap Preview 功能只能支持 8.0 以下系统)。
Java hook 是指通过 YAHFA、epic 等框架,在运行时替换图片创建的相关代码入口函数,从而实现拦截 Bitmap 创建。
以 YAHFA 为例,要拦截 ImageView.setImageBitmap 函数,可以创建一个这样的代理类:
在上面的代码中,通过 className methodName 和 methodSig 可以声明要拦截的具体方法,然后在 hook 函数中,可以执行我们的拦截逻辑,比如记录 Bitmap 的尺寸信息。
这种方式的优点是实现简单,可以拿到的信息较多;缺点是不够稳定,因为底层原理是替换 ArtMethod 的 entryPoint(入口点),由于不同 Android 版本上 ArtMethod 中的结构有变化,因此寻找 entryPoint 的过程需要兼容不同版本,容易出现兼容性问题,只能在线下使用。
在线上要监控图片内存,常用的方式是在编译时通过 AspectJ/ASM/JavaAssit 等方式拦截 Bitmap 创建的代码,在其中统计 Bitmap 的长宽、创建堆栈等信息。
和 JavaHook 不同的在于,编译时修改字节码是修改 APP 中的代码,而不是修改系统的代码,因此稳定性得到了保障。
这种方式的优点是可以获取到比较全面的信息;缺点是需要拦截的代码比较多,需要兼容不同版本的 API,成本较高,同时获取到的堆栈常常是图片加载库的堆栈,无法直接定位到业务代码。
下面是图片创建相关的 API,可以看到涉及的方法很多:
这一节我们了解了常见的图片内存分析方案的优缺点和使用场景。
接下来我们来一看一种更加完善的新方案 Android Bitmap Monitor。
Android Bitmap Monitor 就是今天要介绍的新方案,这一节我们来看下它的功能和实现原理。
Android Bitmap Monitor 是一个开源的 Android 图片内存分析工具,可以帮助开发者快速发现应用内加载的图片是否合理,比如占用内存大小是否合适、是否存在泄漏、缓存是否及时清理、是否加载了当前并不需要的图片等等。
https://github.com/shixinzhang/AndroidBitmapMonitor
支持这些功能:
接下来我们来看下它的三个核心功能的实现原理:
首先,AndroidBitmapMonitor 通过 inline-hook 的方式拦截了 Java Bitmap 对象创建的统一入口,这就避免了前面提到的了运行时 epic hook 和编译时 AOP 拦截的问题–需要兼容不同的图片创建代码。它拦截的哪个入口呢?这需要我们了解下不同版本的 Bitmap 创建流程。
我们知道,为了减少图片内存对应用稳定性的影响,Android 官方对图片的像素数据保存方式做了多次修改,目前的情况是:
这样修改的结果就是,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 对象的释放流程有所不同:
两者的共同点是在执行后 Java Bitmap 的mRecycled 状态会变为 true。因此我们可以通过轮训 Bitmap 对象的 mRecycled 属性来判断这个图片是否被回收,实现方式如下图所示:
通过前面的图片创建流程监控我们拿到了当前创建的所有图片数据,然后可以通过一个线程定时轮训当前拿到的图片对象状态,当发现图片引用被回收或图片对象的 mRecycled 为 true 时,从记录中移除这个图片数据,最后得到的就是没有被回收的图片。
对应的代码位置:https://github.com/shixinzhang/AndroidBitmapMonitor/blob/master/library/src/main/cpp/bitmap_monitor.cpp#L240
上一节对比不同方案时,我们提到有时候图片创建是通过图片库统一完成的,这种情况下获取到的堆栈无法看出业务代码。
这种情况下我们就需要通过图片内容来判断到底是哪里的业务有问题。
可能有小伙伴知道,Android Studio 的 Bitmap Preview 功能是支持查看图片内容的,但很可惜只支持 Android 8.0 以前的设备。这是因为它的实现原理是通过 HPROF 中 Bitmap 对象的 mBuffer 数据,因此只支持 8.0 以前的手机。
AndroidBitmapMonitor 实现了全版本的图片还原功能,根本区别就在于,是从 Native 层做的像素数据获取。
NDK 的 bitmap.h 提供了 AndroidBitmap_lockPixels 函数,通过它我们可以获取到图片的像素数据:
我们知道,图片本质上就是像素点的集合:
遍历像素数据,然后按照通用图片的格式(比如 BMP、PNG)输出为文件,就可以获取到图片的完整内容。
对应的代码位置: https://github.com/shixinzhang/AndroidBitmapMonitor/blob/master/library/src/main/cpp/bitmap_monitor.cpp#L40
好了,到这里我们就了解了图片内存分析新方案 AndroidBitmapMonitor 的实现原理。
AndroidBitmapMonitor 可以为我们提供详细的图片创建信息,基于它可以实现的功能有这些:
源码地址:https://github.com/shixinzhang/AndroidBitmapMonitor
好了,这篇文章到这里就结束了,感谢你的阅读,愿你平安顺遂。