preface

The actuality of TT speech is a great memory, easy to kill retreated to the background process, online there is a certain degree of OOM anomaly, so need to make optimization in the memory, and memory optimization, optimization of Bitmp is indispensable, because this piece of the optimization of the earnings will be very obvious, the following is not the first picture when recovery image memory status, The second image is the memory status after the image is recovered. The Native memory of the second image is obviously less than that of the first one. This is because the memory of the image after Android8.0 is placed in Naitive, so the memory of the Native image will be much less after the image cache is cleared

Analysis of the

If we want to reduce the memory occupied by the Bitmap, we can resampling the Bitmap and reduce the resolution of the image, because we all know that the memory occupied by the image is proportional to the resolution of the image, we can set the width and height of the Bitmap to the same width and height of the control. In theory, As long as the ratio of Bitmap width to control width is 1:1, clarity can be guaranteed. However, for absolute clarity, a ratio of Bitmap width to control width can be allowed to be 1.2:1. Then, we need to find images with Bitmap width to control width ratio greater than 1.2:1. Therefore, we need to do a global monitoring of Bitmap and local resource pictures downloaded online to find out these unreasonable pictures

Monitoring plan

1. Monitoring of Bitmap downloaded online

Monitoring bitmaps downloaded online is relatively easy, because the TT voice is loaded by Fresco, so we can use Fresco to monitor the image download process, obtain the width and height of the Bitmap and the width and height of the control at the end of the Bitmap download, and then compare them. The code is shown below

Object FrescoImageCheck {const val tag = "====>FrescoImageCheck:" var isCanLog = true const val MAX_PROPORTION = 1.2 @jvmstatic fun Check (imageView: SimpleDraweeView, imageInfo: 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) } }Copy the code

If an unreasonable image is detected, the console will raise an alarm message, as shown below, including the Bitmap width, Imageview width, control ID, image load link, etc

2. Monitor the loading of local image resources

The loading of local image resources is more troublesome and needs to be monitored through AOP run-time staking. Here epic is a Java Method granularity run-time AOP Hook framework at the virtual machine level. In simple terms, Epic is Dexposed on ART (support Android 5.0 ~ 11). It can intercept almost any Java method call within the process, and can be used to realize AOP programming, run-time piling, performance analysis, security audit, etc. However, Epic has certain compatibility problems and is not suitable for online use, so it can be used offline, we use Epic. Hook the ImageView setImageDrawable method, either by setting the image in the layout file using SRC, or by calling the ImageView setImageDrawable, setImageResource in Java, SetImageDrawable hook ImageView setImageDrawable hook ImageView setImageDrawable hook ImageView setImageDrawable hook ImageView setImageDrawable hook ImageView setImageDrawable hook ImageView setImageDrawable hook ImageView setImageDrawable hook At the same time, convert Drawable to Bitmap, obtain the width and height of Bitmap, and then compare with the width and height of ImageView to know whether the local image is reasonable, the code is as follows

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.h eight.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.h eight.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, call setImageDrawable setImageResource is eventually DexposedBridge.findAndHookMethod( ImageView::class.java, "setImageDrawable", Drawable::class.java, LocalImageViewCheck() ) } } }Copy the code

If the image is unreasonable, the console will raise an alarm log, as shown below. The log information contains the width and height of the Bitmap, width and height of the ImageView, and control ID

Optimization Suggestions

1. For Fresco loaded images, refer to the alarm information on the console to find any inappropriate images, and resample the Bitmap to the same width and height as the control 2. For local resource pictures, refer to the alarm information of the console to find unreasonable pictures, and then find the new design to cut the picture, set the width and height of the picture to the same width and height as the control 3. Listen for memory usage in Application and picture cache 4 when memory is low. To reduce the possibility that the TT voice is killed in the background, you can clear the image cache when entering the background to reduce the MEMORY usage of the TT voice

QQ communication group of

770892444