Bitmap和本地图片资源优化

910 阅读5分钟

前言

TT语音目前的现状是占用内存大,退至后台容易被杀进程,线上还有一定程度的OOM异常出现,所以,需要对内存做优化,而内存优化中,Bitmp的优化是必不可少的,因为这一块的优化收益会非常明显,下面第一张图片是没有回收图片时候的内存状况,第二张图片是回收图片后的内存状况,第二张图片的Native内存明显比第一张少,这是因为Android8.0以后图片的内存是放在naitive中的,所以在清除图片缓存后Native的内存会少很多

image.png

image.png

分析

如果要降低Bitmap占用的内存,我们可以对bitmap做重采样压缩,降低图片的分辨率,因为我们都知道,图片的内存占用和图片的分辨率是成正比的,我们可以把Bitmap的宽高设置成和控件一样的宽高即可,理论上,只要Bitmap的宽高和控件的宽高的比例达到1:1,就可以保证清晰度,但是,为了保证绝对的清晰,可以允许Bitmap的宽高和控件的宽高的比例达到1.2:1,那么,接下来我们需要找出Bitmap和控件的宽高比是大于1.2:1的图片,所以我们就需要针对线上下载的Bitmap和本地资源图片做一个全局的监控,找出这些不合理的图片

监控方案

1. 线上下载的Bitmap的监控

针对线上下载的Bitmap的监控会比较简单,因为TT语音是通过Fresco加载Bitmap的,所以我们可以使用Fresco监听图片的下载过程,在Bitmap下载结束的时候获取Bitmap的宽高和控件的宽高,然后进行对比即可,代码在如下

object FrescoImageCheck {

    const val tag = "====>FrescoImageCheck:"

    var isCanLog = true

    const val MAX_PROPORTION = 1.2

    /**
     * 检查bitmap的宽高是否大于图片控件宽高的1.2倍,如果大于1.2倍,那说明图片分辨率过高
     */
    @JvmStatic
    fun check(imageView: SimpleDraweeView, imageInfo: ImageInfo, account: String, loadPath: String) {

        if (!AppMetaDataUtil.isInternalBuild(ResourceHelper.getApplication()) || !isCanLog) {
            return
        }

        var imageWidth = imageView.width
        var imageHigh = imageView.height

        var byteSize = 0

        when (imageInfo) {
            is CloseableBitmap -> {
                byteSize = imageInfo.sizeInBytes
            }
            is CloseableImage -> {
                byteSize = imageInfo.sizeInBytes
            }
            is CloseableStaticBitmap -> {
                byteSize = imageInfo.sizeInBytes
            }
        }


        if (imageWidth > 0 && imageHigh > 0) {
            if (imageInfo.width > imageWidth * MAX_PROPORTION
                    && imageInfo.height > imageHigh * MAX_PROPORTION) {
                val stackTrace: Throwable = RuntimeException("Bitmap size too large")
                printWarn(imageView, account, loadPath, imageInfo.width, imageInfo.height, imageWidth, imageHigh, byteSize, stackTrace)
            } else {
                printNormal(imageView, account, loadPath, imageInfo.width, imageInfo.height, imageWidth, imageHigh, byteSize)
            }
        } else {
            imageView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    imageWidth = imageView.width
                    var imageHigh = imageView.height
                    if (imageWidth > 0 && imageHigh > 0) {
                        if (imageInfo.width > imageWidth * MAX_PROPORTION
                                && imageInfo.height > imageHigh * MAX_PROPORTION) {
                            val stackTrace: Throwable = RuntimeException("Bitmap size too large")
                            printWarn(imageView, account, loadPath, imageInfo.width, imageInfo.height, imageWidth, imageHigh, byteSize, stackTrace)
                        }
                        imageView.viewTreeObserver.removeOnPreDrawListener(this)
                    } else {
                        printNormal(imageView, account, loadPath, imageInfo.width, imageInfo.height, imageWidth, imageHigh, byteSize)
                    }
                    return true
                }
            })
        }
    }

    private fun printWarn(imageView: SimpleDraweeView, account: String, loadPath: String, bitmapWidth: Int, bitmapHeight: Int, viewWidth: Int, viewHeight: Int, byteSize: Int, t: Throwable) {
        val warnInfo = StringBuilder("\n")
                .append("\n bitmap widthAndHigh: (").append(bitmapWidth).append('*').append(bitmapHeight).append(')')
                .append("\n imageView widthAndHigh: (").append(viewWidth).append('*').append(viewHeight).append(')')
                .append("\n bitmap byteSize: (").append(byteSize).append(')')
                .append("\n account: (").append(account).append(')')
                .append("\n loadPath: (").append(loadPath).append(')')
//                .append("\n simpleDraweeViewId: (").append(imageView.context.resources.getResourceEntryName(imageView.id)).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString()
        Log.i(tag, warnInfo)
        LogToFileUtils.write(warnInfo)
    }

    private fun printNormal(imageView: SimpleDraweeView, account: String, loadPath: String, bitmapWidth: Int, bitmapHeight: Int, viewWidth: Int, viewHeight: Int, byteSize: Int) {
        val warnInfo = StringBuilder("\nBitmap size is Normal: ")
                .append("\n bitmap widthAndHigh: (").append(bitmapWidth).append('*').append(bitmapHeight).append(')')
                .append("\n imageView widthAndHigh: (").append(viewWidth).append('*').append(viewHeight).append(')')
                .append("\n bitmap byteSize: (").append(byteSize).append(')')
                .append("\n account: (").append(account).append(')')
//                .append("\n simpleDraweeViewId: (").append(imageView.context.resources.getResourceEntryName(imageView.id)).append(')')
                .append("\n loadPath: (").append(loadPath).append(')')
                .toString()
        Log.i(tag, warnInfo)
    }

}

如果监听到不合理的图片,控制台有报警信息抛出,如下所示,控制台会抛出报警信息,包含Bitmap宽高,Imageview宽高,控件id,图片加载链接等

image.png

2. 本地图片资源的加载的监控

针对本地图片资源的加载就比较麻烦,需要通过AOP运行时插桩方式来监控,这里借助了epic,Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 5.0 ~ 11)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等,但是Epic有一定的兼容性问题,不适合带到线上使用,所以在线下使用即可,我们借助了Epic ,hook了ImageView的setImageDrawable方法,不管是通过在布局文件中使用src设置图片,还是在java中调用ImageView的setImageDrawable,setImageResource,setImageBitmap方法设置图片,最终都会调用ImageView的setImageDrawable方法,所以,我们只需要hook ImageView的setImageDrawable方法即可,然后获取ImageView的宽高,同时将Drawable转换成Bitmap,获取Bitmap的宽高,然后再与ImageView的宽高对比,即可知道本地图片是否合理,代码如下

class LocalImageViewCheck : XC_MethodHook() {

    @Throws(Throwable::class)
    override fun afterHookedMethod(param: MethodHookParam) {
        super.afterHookedMethod(param)
        if (param.thisObject !is SimpleDraweeView) {
            val imageView = param.thisObject as ImageView
            if (param.method.name == "setImageDrawable") {
                checkDrawable(imageView, param.args[0] as Drawable)
            }
        }
    }

    private fun checkDrawable(imageView: ImageView, drawable: Drawable) {
        if (drawable is BitmapDrawable) {
            val bitmap = drawable.bitmap
            checkIllegalBitmap(imageView, bitmap)
        }
    }

    private fun checkIllegalBitmap(imageView: ImageView, bitmap: Bitmap) {
        if (bitmap != null) {
            val imageViewWidth = imageView.width
            val imageViewHeight = imageView.height
            if (imageViewWidth > 0 && imageViewHeight > 0) {
                if (bitmap.width >= imageViewWidth * MAX_PROPORTION
                        && bitmap.height >= imageViewHeight * MAX_PROPORTION) {
                    val stackTrace: Throwable = RuntimeException("local icon size too large")
                    printWarn(bitmap, imageView, stackTrace)
                } else {
                    printNormal(bitmap, imageView)
                }
            } else {
                imageView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
                    override fun onPreDraw(): Boolean {
                        val imageViewWidth = imageView.width
                        val imageViewHeight = imageView.height
                        if (imageViewWidth > 0 && imageViewHeight > 0) {
                            if (bitmap.width >= imageViewWidth * MAX_PROPORTION
                                    && bitmap.height >= imageViewHeight * MAX_PROPORTION) {
                                val stackTrace: Throwable = RuntimeException("local icon size too large")
                                printWarn(bitmap, imageView, stackTrace)
                            } else {
                                printNormal(bitmap, imageView)
                            }
                            imageView.viewTreeObserver.removeOnPreDrawListener(this)
                        }
                        return true
                    }
                })
            }
        }
    }

    private fun getBitmapByteCount(bitmap: Bitmap): Int {
        return bitmap.rowBytes * bitmap.height
    }

    @SuppressLint("LongLogTag")
    private fun printWarn(bitmap: Bitmap, imageView: ImageView, stackTrace: Throwable) {
        val warnInfo = StringBuilder()
                .append("\n bitmap widthAndHigh: (").append(bitmap.width).append("px").append('*').append(bitmap.height).append("px").append(')')
                .append("\n imageView widthAndHigh: (").append(ScreenUtils.pxToDp(imageView.width.toFloat())).append("dp").append('*').append(ScreenUtils.pxToDp(imageView.height.toFloat())).append("dp").append(')')
                .append("\n bitmap byteSize: (").append(getBitmapByteCount(bitmap)).append(')')
                .append("\n suggest bitmap byteSize: (").append(imageView.width * MAX_PROPORTION).append('*').append(imageView.height * MAX_PROPORTION).append(')')
                .append("\n imageView: (").append(imageView.toString()).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(stackTrace)).append('\n')
                .toString()
        Log.i(tag, warnInfo)
    }

    @SuppressLint("LongLogTag")
    private fun printNormal(bitmap: Bitmap, imageView: ImageView) {
        val normalInfo = StringBuilder("local icon size is Normal: \n")
                .append("\n bitmap widthAndHigh: (").append(bitmap.width).append("px").append('*').append(bitmap.height).append("px").append(')')
                .append("\n imageView widthAndHigh: (").append(ScreenUtils.pxToDp(imageView.width.toFloat())).append("dp").append('*').append(ScreenUtils.pxToDp(imageView.height.toFloat())).append("dp").append(')')
                .append("\n bitmap byteSize: (").append(getBitmapByteCount(bitmap)).append(')')
                .append("\n imageView: (").append(imageView.toString()).append(')').append('\n')
                .toString()
        Log.i(tag, normalInfo)
    }

    companion object {

        var isCanHook = true
        const val tag = "====>LocalImageViewCheck:"

        fun hook() {

            if (!BuildConfig.DEBUG) {
                return
            }

            // setImageBitmap,setImageResource 最终也是调用 setImageDrawable
            DexposedBridge.findAndHookMethod(
                    ImageView::class.java,
                    "setImageDrawable",
                    Drawable::class.java,
                    LocalImageViewCheck()
            )
        }
    }

}

如果图片是不合理的,控制台会有报警日志抛出,如下所示,日志信息包含了Bitmap的宽高,ImageView的宽高,控件id

image.png

优化建议

1.针对Fresco加载的图片,参考控制台的报警信息,找到不合理的图片,然后设置重采样,将Bitmap宽高设置成与控件一样的宽高即可
2.针对本地资源图片,参考控制台的报警信息,找到不合理的图片,然后找设计从新要切图,将图片宽高设置成与控件一样的宽高即可
3.在Application中监听内存使用情况,在内存不足的时候情况图片缓存
4.为了降低TT语音在后台被杀进程的可能性,可以在进入后台的时候清空图片缓存,降低TT语音的内存占用

QQ交流群

770892444