彻底解决Glide 偶现 trying to use a recycled bitmap 异常

1,445 阅读3分钟

问题产生原因

glide 在播放 gif的时候 极低概率 会出现 类似 如下日志的异常:

java.lang.RuntimeException	Canvas: trying to use a recycled bitmap android.graphics.Bitmap@f3ec31f
BaseCanvas.java	55
java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@f3ec31f
	at android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:55)
	at android.view.DisplayListCanvas.throwIfCannotDraw(DisplayListCanvas.java:226)
	at android.view.RecordingCanvas.drawBitmap(RecordingCanvas.java:97)
	at com.bumptech.glide.load.resource.d.e.draw(SourceFile:297)

经过问题定位 发生在

这个问题 不管在 glide issue上还是谷歌搜搜上都没有找到产生的具体原因,虽然概率极低极低 但是问题总要解决的吗 ,先想办法 让他不崩,然后再想办法后续优化

如何解决

要解决这个问题 最简单快捷的思路就是 在这个出问题的代码上 做try-catch了。 一劳永逸。 类似于这种第三方库要try-catch 一开始想到的时候 下载对应版本的源码 修改以后 再重新编译出新的aar。 但是这样做 有点麻烦,而且 扩展性也不好,别的模块 未必想用你魔改的版本 只想用原生版本。

那就只剩最后一条路了,利用字节码修改技术 我们直接在编译的时候 修改这个类的class文件 帮他加个try-catch不就行了?

这里的字节码修改 我选用的是javaassist。注意利用javaassist 给一个函数加上try-catch 是有坑的,他原生提供了addCatch 这个方法:

但是这个方法 要求你必须 try catch 以后必须throw 否则一直报错。。。throw出去不就等于没catch住吗

同时如果你直接用insertBefore 插入try catch 也是不行的,因为 他还是会报错。。。

所以我们得用稍微麻烦的办法。

创建一个新的方法 tryDraw 他里面的内容和draw方法里的内容一摸一样。 然后将draw方法里的内容改写成 调用我们的tryDraw方法 只不过在这个调用tryDraw的地方 加上try-catch 即可。

transform怎么写

直接上代码把:

package com.vivo.space.detect.javassist

import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import com.vivo.space.detect.utils.AndroidSdk
import com.vivo.space.detect.utils.ProjectFileRead
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import javassist.CtNewMethod
import org.gradle.api.Project
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

/**
 *  这个tf 用来处理 第三方库的 问题,可以直接修改字节码 很方便。
 */
class HelpThirdClassTransform(project: Project) : Transform() {

    val project = project

    override fun getName(): String {
        return "HelpThirdClassTransform"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 不支持增量构建
     */
    override fun isIncremental(): Boolean {
        return false
    }

    /**
     * 由于 只支持 处理 第三方库的 代码 所以这里对影响范围做了特殊处理
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return HashSet<QualifiedContent.Scope>().apply {
            add(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
        }
    }

    override fun transform(transformInvocation: TransformInvocation) {
        println("---------------HelpThirdClassTransform transform start !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
        val outputProvider = transformInvocation.outputProvider
        //删除上一次构建的产物信息 因为这个transform 是不支持增量构建的
        outputProvider.deleteAll()
        val androidSdkPath = AndroidSdk.getAndroidJar(ProjectFileRead.getCompileSdkVersion(project)).absolutePath
        println("-------------androidSdkPath:     $androidSdkPath")
        //处理全部class的输入
        transformInvocation.inputs.forEach { input ->
            //处理jar包
            input.jarInputs.forEach { jarInput ->

                //首先 把 jar包 拷贝到 固定的路径下
                val dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                jarInput.file.copyTo(dest, true)

                //拷贝到目标路径以后 再对这个jar包 进行处理
                var ctPool = ClassPool()
                //首先把android.jar 加入到classPath中 否则很多android包下面的方法你调用不了
                ctPool.appendClassPath(androidSdkPath)
                //其次把我们这个这个jar包 也放到我们的classPath中
                ctPool.appendClassPath(dest.absolutePath)
                //声明一个变量 看看是否能获取到glideClass
                var glideClass: CtClass
                try {
                    //看看是否能找到GifDrawable这个class 找不到 那就说明这个jar 不是glide的jar 那就肯定会抛异常
                    //那就跳出循环就可以了
                    glideClass = ctPool.getCtClass(GLIDE_GIF_CLASS_NAME)
                } catch (e: Exception) {
                    return@forEach
                }
                // 能走到这 就证明找到 GifDrawable这个class了 ,此时我们直接取获取draw方法
                val drawMethod: CtMethod = glideClass.getDeclaredMethod(METHOD_NAME_DRAW)
                println("HelpThirdClassTransform :找到draw方法了")
                //复制一个名为try-draw的新方法
                val newMethod: CtMethod = CtNewMethod.copy(drawMethod, NEW_METHOD_NAME_TRY_DRAW, glideClass, null)
                glideClass.addMethod(newMethod)
                //这里就将原来的draw方法的内容 替换成 调用我们的tryDraw方法 并在调用tryDraw的地方 加上try catch代码块
                val sb = StringBuffer()
                sb.append("{try{")
                sb.append(NEW_METHOD_NAME_TRY_DRAW)
                //这里要传递canvas这个参数 注意javaassist的写法
                sb.append("(\$1)")
                sb.append(";}catch(Exception e){  android.util.Log.e(\"GifDrawable\", \"draw\", e);}")
                sb.append("}")
                //改写我们的draw方法
                drawMethod.setBody(sb.toString())
                //首先获取一下 这个glide所属的jar包名称是什么 一般而言都是纯数字的 例如 56.jar 等等
                val glideJarName = dest.name
                println("glideJarName:" + glideJarName + "  absolutePath:" + dest.absolutePath + " canonicalPath:" + dest.canonicalPath)
                val destZipFile = ZipFile(dest)
                //既然要替换原先的jar包 那就要首先创造一个新的jar包,命名就将之前的 56.jar 替换成 56wuyue.jar 即可
                val newName = dest.absolutePath.replace(".jar", "wuyue.jar")
                println("newName:$newName")
                val zos = ZipOutputStream(FileOutputStream(newName))
                //这一步就开始创造我们的新的jar包了,将我们不想改的文件 直接复制到新的jar包中
                //唯一要改的文件 单独处理
                destZipFile.entries().asSequence().forEach {
                    zos.putNextEntry(ZipEntry(it.name))
                    try {
                        if (it.name == GLIDE_GIF_CLASS_ENTRY_NAME) {
//                            println("写入新的gif class------------")
                            zos.write(glideClass.toBytecode())
                        } else {
//                            println("写入老的的gif class------------")
                            zos.write(destZipFile.getInputStream(it).readBytes())
                        }
                    } catch (e: Exception) {
                        println("destZipFile forEach error:" + e.message)
                    }
                    zos.closeEntry()
                }
                try {
                    zos.close()
                    //将原来的 56.jar 删除
                    println("删除原文件:" + dest.delete())
                    //把我们新创建的 56wuyue.jar 重新命名为 之前的56.jar 即可 整个流程到这里就结束了
                    println("文件重命名:" + File(newName).renameTo(File(dest.absolutePath)))
                } catch (e: Exception) {
                    println("destZipFile e:" + e.message)
                }

            }

        }
        println("---------------HelpThirdClassTransform transform end !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")


    }

    companion object {
        //GifDrawable的全名 包含他所属的包名
        const val GLIDE_GIF_CLASS_NAME = "com.bumptech.glide.load.resource.gif.GifDrawable"

        //jar包中的entry name 注意和实际的类名 是有区别的
        const val GLIDE_GIF_CLASS_ENTRY_NAME = "com/bumptech/glide/load/resource/gif/GifDrawable.class"


        //要修改的是GifDrawable的 draw方法
        const val METHOD_NAME_DRAW = "draw"

        //新方法命名为tryDraw
        const val NEW_METHOD_NAME_TRY_DRAW = "tryDraw"

    }
}

最后看下效果: