加载图片是一个很常规的操作,同时也是一个“成本”较高的行为,因为加载一张图片可能需要先后历经 网络请求、I/O 读写、内存占用 等多个过程。我们一般是通过 Coil、Glide 等开源库来加载图片,完全无需关心其加载过程,而其中可能就隐藏着一个不是很合理的情况:加载的图片属于大图,超出了展示所需
加载展示所需的图片会造成不必要的性能浪费,同时也可能会引发 OOM,因此进行应用性能优化的一个点就是检测应用全局的图片加载情况,本文就来介绍如何通过字节码插桩的方式来实现全局大图检测
首先,什么类型的图片属于大图呢?我觉得可以从两个方面来进行认定:
- 图片的尺寸大于 ImageView 本身的尺寸。例如,ImageView 的宽高只有 100 dp,但图片却有 200 dp
- 图片的大小超过一定阈值。例如,我们可以规定单张图片最多不能超出 1 MB,大于该值的图片就认为是大图
我们项目中使用的 ImageView 的类型又可以分为两种:
- 系统内置的android.widget.ImageView。一般是在 XML 文件中通过 <ImageView/> 标签来进行使用
- 开发者自定义的 ImageView 子类。一般也是在 XML 文件使用
因此,基本的实现思路就是:通过定义一个统一的 ImageView 供项目全局使用,用于替代系统内置的 ImageView 和各个自定义子类的直接父类,当 setImageDrawable 和 setImageBitmap 两个方法被调用时,就对 Drawable 的尺寸和大小进行检测,当检测到属于大图时就按照实际的业务情况进行数据上报
open class MonitorImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : android.widget.ImageView(context, attrs, defStyleAttr), MessageQueue.IdleHandler {
companion object {
private const val MAX_ALARM_IMAGE_SIZE = 1024
}
override fun setImageDrawable(drawable: Drawable?) {
super.setImageDrawable(drawable)
monitor()
}
override fun setImageBitmap(bm: Bitmap?) {
super.setImageBitmap(bm)
monitor()
}
private fun monitor() {
Looper.myQueue().removeIdleHandler(this)
Looper.myQueue().addIdleHandler(this)
}
override fun queueIdle(): Boolean {
checkDrawable()
return false
}
private fun checkDrawable() {
val mDrawable = drawable ?: return
val drawableWidth = mDrawable.intrinsicWidth
val drawableHeight = mDrawable.intrinsicHeight
val viewWidth = measuredWidth
val viewHeight = measuredHeight
val imageSize = calculateImageSize(mDrawable)
if (imageSize > MAX_ALARM_IMAGE_SIZE) {
log(log = "图片大小超标 -> $imageSize")
}
if (drawableWidth > viewWidth || drawableHeight > viewHeight) {
log(log = "图片尺寸超标 -> drawable:$drawableWidth x $drawableHeight view:$viewWidth x $viewHeight")
}
}
private fun calculateImageSize(drawable: Drawable): Int {
return when (drawable) {
is BitmapDrawable -> {
drawable.bitmap.byteCount
}
else -> {
0
}
}
}
private fun log(log: String) {
Log.e(javaClass.simpleName, log)
}
}
当然,我们也不太可能采取硬编码的方式来直接修改项目中的原有逻辑,成本太高,不灵活,而且也无法照顾到外部依赖。此时通过字节码插桩的方式来实现就成了比较经济和高效的方案,可以做到多项目复用
对于开发者自定义的 ImageView 子类,我们只需要在 Transform 阶段,当检查到当前 Class 直接继承于系统的 ImageView,就将其改为继承于 MonitorImageView 即可。稍微麻烦一点的是在 XML 中声明的 <ImageView/> 标签
我们知道,在布局文件中声明的各个控件,在使用时都对应一个个具体的 View 实例对象,而想要将静态的 XML 声明转换为动态的实例对象,就需要通过解析 XML 文件并根据类路径来反射出实例对象了,这一部分逻辑就隐藏在 LayoutInflater 中,LayoutInflater 会根据我们传入的 layoutResID 来进行解析
另一方面,现如今我们在新建 Activity 时,一般都不会直接继承于系统内置的 android.app.Activity,而是使用 androidx.appcompat.app.AppCompatActivity,AppCompatActivity 提供了更多的兼容性保障,当中就包含了自定义实现的 LayoutInflater
AppCompatActivity 通过 AppCompatViewInflater 来解析 XML 文件,当判断到我们声明的是系统控件时(例如 TextView、ImageView、Button 等),就会使用对应的 AppCompatXXX 来生成相应的实例对象,ImageView 就对应 AppCompatImageView
所以说,大多数情况下我们使用的 ImageView 实例对应的其实都是 androidx.appcompat.widget.AppCompatImageView,而非 android.widget.ImageView。这就为我们提供了一个 hook 点:只要我们能够将 AppCompatImageView 的父类修改为我们自定义的 MonitorImageView,就可以来为应用全局实现一个统一的大图检测功能了
有了上述思路后,相应的插桩代码也就很简单了
class LegalBitmapTransform(private val config: LegalBitmapConfig) : BaseTransform() {
private companion object {
private const val ImageViewClass = "android/widget/ImageView"
}
override fun modifyClass(byteArray: ByteArray): ByteArray {
val classReader = ClassReader(byteArray)
val className = classReader.className
val superName = classReader.superName
Log.log("className: $className superName: $superName")
return if (className != config.formatMonitorImageViewClass && superName == ImageViewClass) {
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
val classVisitor = object : ClassVisitor(Opcodes.ASM6, classWriter) {
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(
version,
access,
name,
signature,
config.formatMonitorImageViewClass,
interfaces
)
}
}
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
classWriter.toByteArray()
} else {
byteArray
}
}
}
本文暂时没有评论,来添加一个吧(●'◡'●)