关于使用javassist-ClassPool在gradle插件中修改某个类被重复调用的bug(缓存问题)
背景描述
由于一些第三方库,如ARouter或者Retrofit封装得不够完善,即难以自己写一个类似的库,又在互联网上找不到类似的库。那么可以考虑动态修改这类第三方库,插入一些自己需要的代码或者监听器。相比静态修改(拉取别人的代码,再修改)的优势在于,当这类第三方库出新版时,可以平滑升级,而不需要考虑任何其它因素。(一般而言,只要你动态修改的是第三方库的public方法)
异常现象
比如,我只想修改Postcard的navigation方法。在他在前面插入一条打印语句。结果,当先执行clean,再执行debug run,再执行rebuild时,navigation方法出现了两条打印语句。
异常原因
产生此类异常的原因是android studio在执行上述一系统操作时,并不是每次都重新加载相关类,并运行,而是有静态缓存的。而getDefault返回的就是被静态引用的那个类。而CtClass也有自己的缓存机制。故而,上述一系统操作对某个类的修改就重复了。
public Object navigation() {
System.out.println("我是已经被修改过的ARouter...");
System.out.println("我是已经被修改过的ARouter...");
return this.navigation((Context)null);
}
问题描述
使用javassist修改某个类,示例代码如下:
下面出问题的代码就出在了:
1、调用了ClassPool pool = ClassPool.getDefault()
2、并且没有调用ctClass.detach()
解决方案:
1、修改完CtClass后,记得调用ctClass.detach()(推荐)
2、每次需要ClassPool时使用new ClassPool(),而非使用 ClassPool.getDefault()
void modifyJar(JarInput jarInput,String destAbsolutePath){
//只修改ARouter相关class
if (!jarInput.file.absolutePath.contains("arouter-api")){
return
}else {
println "准备修改jar包:from->" + jarInput.file.absolutePath + " destAbsolutePath->" + destAbsolutePath
}
//每次直接使用 getDefault 而不使用new ClassPool()的方式或者不使用CTClass的detach方法会导致缓存问题。
//即,上次run时修改过的输出类,被作为下次rebuild时的输入类(由于android studio的静态变量缓存问题)
// ClassPool pool = new ClassPool()
//添加java基础包
// pool.appendSystemPath()
ClassPool pool = ClassPool.getDefault()
pool.insertClassPath(destAbsolutePath)
//获取临时解压的文件路径
File tmpDir = new File(mProject.buildDir,"myTmpDir")
if (tmpDir.exists()){
println "临时文件夹存在,准备删除临时文件夹 " + tmpDir
FileUtils.deleteDirectory(tmpDir)
}else {
println "不存在旧的临时文件夹,不需要删除... " + tmpDir
}
println "准备创建临时文件夹 " + tmpDir
tmpDir.mkdirs()
//将原jar包解压
Decompression.uncompress(destAbsolutePath,tmpDir.absolutePath)
//获取准备修改的类文件
CtClass ctClass = pool.get("com.alibaba.android.arouter.facade.Postcard")
//对于已经冻结的class解冻之后还可以继续编辑修改
if (ctClass.isFrozen()){
ctClass.defrost()
}
//获取需要修改的方法
// CtMethod[] ctMethods = ctClass.getDeclaredMethods()
// println ">>>>>>>>>> ctMethods: " + ctMethods
CtMethod ctMethod = ctClass.getDeclaredMethod("navigation")
println ">>>>>>>>>>> ctMethod: " + ctMethod
ctMethod.insertBefore(injectStr)
//将修改后的Class写入到临时目录
ctClass.writeFile(tmpDir.absolutePath)
FileUtils.forceDelete(new File(destAbsolutePath))
//压缩
Compressor.compress(destAbsolutePath, tmpDir.absolutePath)
//清除缓存(如果不清除缓存,会导致同一个class被反复修改)
// ctClass.detach()
println "压缩成功"
}