问题产生原因
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"
}
}
最后看下效果: