Android 基于Glide的Bitmap监控

前言

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。

相关推荐
wordbaby1 分钟前
TanStack Router 文件命名约定
前端
打工人小夏1 分钟前
vue3使用transition组件,实现过度动画
前端·vue.js·前端框架·css3
LFly_ice4 分钟前
Next-1-启动!
开发语言·前端·javascript
小时前端6 分钟前
谁说 AI 历史会话必须存后端?IndexedDB方案完美翻盘
前端·agent·indexeddb
wordbaby11 分钟前
TanStack Router 基于文件的路由
前端
wordbaby15 分钟前
TanStack Router 路由概念
前端
wordbaby18 分钟前
TanStack Router 路由匹配
前端
cc蒲公英19 分钟前
vue nextTick和setTimeout区别
前端·javascript·vue.js
程序员刘禹锡23 分钟前
Html中常用的块标签!!!12.16日
前端·html
丐中丐99932 分钟前
一个Binder通信中的多线程同步问题
android