前言
TT语音目前的现状是占用内存大,退至后台容易被杀进程,线上还有一定程度的OOM异常出现,所以,需要对内存做优化,而内存优化中,Bitmp的优化是必不可少的,因为这一块的优化收益会非常明显,下面第一张图片是没有回收图片时候的内存状况,第二张图片是回收图片后的内存状况,第二张图片的Native内存明显比第一张少,这是因为Android8.0以后图片的内存是放在naitive中的,所以在清除图片缓存后Native的内存会少很多
分析
如果要降低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,图片加载链接等
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
优化建议
1.针对Fresco加载的图片,参考控制台的报警信息,找到不合理的图片,然后设置重采样,将Bitmap宽高设置成与控件一样的宽高即可
2.针对本地资源图片,参考控制台的报警信息,找到不合理的图片,然后找设计从新要切图,将图片宽高设置成与控件一样的宽高即可
3.在Application中监听内存使用情况,在内存不足的时候情况图片缓存
4.为了降低TT语音在后台被杀进程的可能性,可以在进入后台的时候清空图片缓存,降低TT语音的内存占用
QQ交流群
770892444