前言
Bitmap 是Android中最重要位图数据组件,承载着各种UI展示、图像处理等功能。与之一起使用的还有BitmapFactory、Canvas软绘制,以及Android 9.0新增的ImageDecoder。
接下来我们先回顾一下Bitmap一些性质和用法:
Bitmap 有很多像素格式:
ALPHA_8: 只包含alpha颜色通道的格式,占8bit,一般用于遮罩(MASK)图像的生存。 RGB_565: 只包含RGB通道,总共16bit (5+6+5),一般用于非透明图像的的压缩和封装。 ARGB_4444: 包含全部通道,总共16bit (4x4),一般用于清晰度不太高的场景,目前已属于过期类型。 ARGB_8888: 包含全部通道,总共32bit (4x8),一般用于色彩丰富的图像展示 RGBA_F16: 包含全部通道,总共64bit (4x16),一般用于高清大图展示。 HARDWARE:包含全部通道,大小方面取决于原图类型,这种类型适合硬件加速方式的绘制。
Bitmap 支持图像处理:
Bitmap 本身可以缩放、平移、旋转、镜像、合成、颜色调整等,同时也支持绘制、增强,当然Hardware类型除外。
java
Canvas canvas = new Canvas(bitmap);
canvas.drawXXX(...)
Bitmap 支持复用:
复用一般分为两种,一种是缓存复用,另一种是Bitmap缓冲区复用:
缓存复用:
一般是基于缓存机制的复用,如网络缓存、内存、磁盘缓存,这里你一定相当了常用的算法,lfu、lru、fifo等,不过目前来说也有非常完善的开源框架如fesco、glide等,其中glide在lfu、lru缓存方面更加完善。
Bitmap缓冲区复用:
Bitmap如果是mutable的,在解码其他图片时可以对进行复用,也就是Options.inBitmap设置Bitmap,因为Bitmap的创建时性能损耗相对比较大,其次如果过于频繁创建和recycle可能造成Android 8.0之前的版本产生碎片,引发OOM。因此,缓冲区复用是一项优化手段,当然前提是可变图片(mutable),因为这种图片可以"擦除"原有的脏数据,只需要bitmap.easeColor(Color.Transparent)即可。但要注意的是,这种使用仅仅支持jpeg、png图片,其次会覆盖inPreferredConfig偏好设置。
Bitmap 会自动回收:
bitmap#recycle是重要的,但其实他也能自动回收,这里可以参考ResourcesImpl中的Drawable Cache的实现,没有任何recycle。不过这也是造成OOM的原因之一,主动recycle的调用比自动回收效果要好,因此开发中要尽可能回收任何不再使用bitmap。
另外Bitmap自带回收机制,Android 8.0之前的版本基于堆分配,通过finalize机制实现回收,Android 8.0之后使用的Cleaner + 虚引用队列实现。为什么是8.0? 我们知道,jdk 1.8之前的虚引用存在一些缺陷,在对象要被回前通知引用队列,造成回收不及时,jdk 1.8之后解决了这个问题,android 8.0,默认使用的是jdk 1.8,因此有效解决了引用问题。
Bitmap 解码方法效率存在差异:
Bitmap各种解码方法中,有些实现其实是比较耗时的,不过其中decodeStream性能相对要好。不过解码过程中需要回溯一些信息,在早期的Android版本中,InputStream中mark和reset操作存在问题,很难完成回溯,因此需要进行单独适配。
另外可以考虑BufferedInputStream,方便解码过程回溯,以提升效率。
Bitmap 放大存在风险
首先,Canvas 绘制的Bitmap不允许过大,一般来说超过100M就会出现绘制异常。而问题是,如果通过BitmapFactory去放大图片,这种风险的概率会陡增,因此对于图片的放大建议使用createScaleBitmap,如果是自定义View,建议使用Matrix缩放 ,对于放大产生的模糊和马赛克,建议使用Piant双线型过滤 + Pain抖动方式去处理。
以上就是Bitmap一些性质用法,下面我们回到主题: 使用实现Bitmap监控。
Bitmap 监控
首先,我们监控的是内存图片;其次,大多数情况下,大部分造成OOM的问题的情况属于APK以外的图片,如果是apk内置图片往往能在上线前包体积优化阶段处理,可借助profiler、perftto、mat、strictMode等大量工具处理。
工具梳理
我们先来对比下市面上常见的一些Bitmap 监控工具:
- dumpheap类工具:这类主要是通过dumpheap + shark实现的,主要是通过阈值、泄露等机制去实现的,不过难点是阈值的准确程度设定需要收集大量数据。
- Native Hook: 通过hook native层 Bitmap的创建和回收实现,理论上可以监控到任何bitmap,覆盖面也比较广。
- ASM字节码:这类我了解到有两种,一种是字节码拦截ImageView#setImageXXX方法,另一种是hook Glide、Fesco、Okhttp、httpUrlconnection,但上两种方法有些怪异,第一种我们完全可以用LayoutInflater.Factory2去替换ImageView,不过第一种也还能理解,毕竟不是所有的View都会走xml,而第二种去Hook开源代码做法显然太奇怪,毕竟人家都开源给你了,另外httpUrlConnection可以使用URL StreamHandler路由机制直接替换为okhttp,去hook【开源代码】的意义在哪里呢 ?
假设 & 结论
在java/kotlin层监控Bitmap其实难度很大,不可能面面俱到,我们本篇使用Glide来监控图片,当然本身也有其缺陷,不过我们来做基于现状的假设:
- 在app中,图片加载框架一般只有一种
- 在app中,最重要的是监控网络图片
- 在app中,通过图片框架的本地图片、系统资源也能被监控
那么,Glide和Fesco不仅仅可以加载网络图片、资源文件、asset目录文件、磁盘文件都能加载,不过在很多app中,Glide的覆盖面显然高一些。本篇我们以Glide为主,至于其他开源框架,代码都是开源的,参考实现即可。对于开源框架,改源码的效率显然要快于ASM修改字节码方式。
下面,我们使用Glide来实现图片监控
原理 & 实现
确定方案
Glide作为一个扩展性极高的框架,不仅仅支持Transform扩展,也支持网络引擎、线程池、缓存池、解码器扩展,不过,有一些不允许扩展的类,其中包括GlideBuilder,而GlideBuilder 被单例对象持有,但其内部也提供了诸多接口。
我们要监控Bitmap,显然得借助GlideBuilder,第一种方案是替换加载引擎,不过这个方法显然不是public的,当然也可以去改源码或者通过"包名hack"去实现。
java
// For testing.
GlideBuilder setEngine(Engine engine) {
this.engine = engine;
return this;
}
不过,这种方法意义不那么大了,因为Glide还提供了另一个方法
java
@NonNull
public GlideBuilder addGlobalRequestListener(@NonNull RequestListener<Object> listener) {
if (defaultRequestListeners == null) {
defaultRequestListeners = new ArrayList<>();
}
defaultRequestListeners.add(listener);
return this;
}
显然Global意味着这个是可以全局使用的,而监控Bitmap的核心也是RequestListener。
java
public interface RequestListener<R> {
boolean onLoadFailed(
@Nullable GlideException e,
@Nullable Object model,
@NonNull Target<R> target,
boolean isFirstResource);
boolean onResourceReady(
@NonNull R resource,
@NonNull Object model,
Target<R> target,
@NonNull DataSource dataSource,
boolean isFirstResource);
}
}
Monitor实现
那么,很显然,我们要实现这个接口,其次设置给GlideBuilder,我们这里简单实现一下,不过要注意的要使用弱引用持有图片,防止Bitmap无法自动回收。
另外也要避免阻塞UI线程,我们这里将数据在子线程单独处理。
java
public class BitmapMonitor implements RequestListener<Object>, Handler.Callback {
private static final int MSG_BITMAP_READY = 1;
ArrayMap<Object, WeakReference<Bitmap>> bitmapReference = new ArrayMap<>();
HandlerThread handlerThread = new HandlerThread("BitmapMonitor"); //防止阻塞UI线程
Handler handler;
public BitmapMonitor() {
handlerThread.start();
handler = new Handler(handlerThread.getLooper(), this);
}
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<Object> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Object resource, Object model, Target<Object> target, DataSource dataSource, boolean isFirstResource) {
Message.obtain(handler, MSG_BITMAP_READY, Pair.create(resource, model)).sendToTarget();
return false;
}
@Override
public boolean handleMessage(Message msg) {
int what = msg.what;
switch (what) {
case MSG_BITMAP_READY:
onBitmapReady(msg);
break;
}
return false;
}
private void onBitmapReady(Message msg) {
Object obj = msg.obj;
if (!(obj instanceof Pair)) {
return;
}
Pair<Object, Object> pair = (Pair<Object, Object>) obj;
if (pair.first instanceof Bitmap) {
bitmapReference.put(pair.second, new WeakReference<Bitmap>((Bitmap) pair.first));
} else if (pair.first instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) pair.first).getBitmap();
bitmapReference.put(pair.second, new WeakReference<Bitmap>((Bitmap) bitmap));
}
}
/***省略一些你觉得可能没有必要的代码***/
}
注册
我们要给GlideBuilder设置Global Listener就需要借助AppGlideMoudle去实现,不过要注意的是, annotationProcessor 需要配置,不然无法生效
java
@GlideModule
public class AppGlideConfigModule extends AppGlideModule {
@Override
public boolean isManifestParsingEnabled() {
return false;
}
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.addGlobalRequestListener(new BitmapMonitor());
}
}
到这里核心逻辑就结束了,我们通过Glide加载的任意资源文件,不限于网络图片,本地图片、系统资源、相册资源都能被监控。
当然,你可能会说,ImageView#setImageXXX无法被监控。我们开头说过,我们主动设置的图片在包体积优化阶段就你清楚的知道大小,除非你没有做包体积优化,另外我们遇到大图OOM问题,基本都是来自APK以外的资源,特别是线上资源。java层做不到native hook全部监控的方式,但我们可以抓重点。另外一方面,hook、asm的维护成本要比编码方式高的多,当然,适合你的场景才是最好的。
总结
本篇主要是基于Glide实现了无hook、非侵入的Bitmap监控,对于其他框架也可以参考实现。对于开源代码,理论上任何人都可以去修改,很多开发者看开源代码也仅仅是看开源代码,开源代码中有很多优秀的设计,我们可以利用这种开源实现app的功能和性能优化,对于开源代码本身的问题,建议还是修改源码的方式实现,而不是去hook。