ASM 位元組碼插樁:監控大圖載入
載入圖片是一個很常規的操作,同時也是一個“成本”較高的行為,因為載入一張圖片可能需要先後歷經 網路請求、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
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"
private const val AppCompatImageViewClass = "androidx/appcompat/widget/AppCompatImageView"
}
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 ((superName == ImageViewClass && className != config.formatMonitorImageViewClass) || className == AppCompatImageViewClass) {
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
}
}
}
最後也給出完整的原始碼:ASM_Transform