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。

相关推荐
汪子熙11 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ19 分钟前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
500了3 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵4 小时前
Android Debug Bridge(ADB)完全指南
android·adb
Мартин.4 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd6 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer6 小时前
Vite:为什么选 Vite
前端