Android Gradle(九)自定义Transform插入和删除代码
一、前言
在安卓中,难免会碰到需要动态插入代码,或者删除代码。这时候就需要用到自定义Transform任务,来对Class文件进行扫描和获取。
- 可以插入的代码场景有哪些?例如:打印方法的执行时间等。
- 可以删除的代码场景有哪些?例如:把代码中Log.e日志打印的代码去掉等
先看下代码前后效果,原始代码如下:
经过修改后(通过反编译apk得到源码):
接下来,让我们看看如何对以上两个场景进行代码的插入和删除。
二、工程准备
2.1 基础知识预备
1.需要用到Javassist来修改代码,不会的可以看看如何使用
2.需要用到自定义插件
如果你已经有了上面两个基础后,那我们就可以开始搭建工程了。
2.2 创建Transform
创建一个Transform用来对代码进行处理
流程如下:
- 从Transform中拿到我们需要处理的Class文件路径
- 从Class文件路径中找到Class文件结尾的文件名(因为可能有)
- 找到需要的Class文件对其代码进行插入和修改
- 除了代码的插入和修改,其他基本都是模板代码,不要纠结。😀
class MyTransform extends Transform { def project def pool = ClassPool.default MyTransform(Project project) { this.project = project } //任务名 @Override public String getName() { return "MyTransform"; } //你想要处理的文件 @Override public Set getInputTypes() { return TransformManager.CONTENT_CLASS; } //你想要处理的范围 @Override public Set getScopes() { return TransformManager.SCOPE_FULL_PROJECT; } //是否增量编译 @Override public boolean isIncremental() { return false; } @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation); println "start transform" //1.拿到需要的处理的class文件 transformInvocation.getInputs().each { allInput -> //类最终生成为两种形式 1.文件夹(包含包名) 2.jar包 //1.1 先从文件夹中拿到我们需要Class文件 allInput.directoryInputs.each { dirInput -> def preClassNamePath = dirInput.file.absolutePath println "class文件路径"+preClassNamePath //插入文件路径到Pool内存池 pool.insertClassPath(preClassNamePath) findTarget(dirInput.file,preClassNamePath) //1.2 获取输出的文件夹 def dest = transformInvocation.outputProvider.getContentLocation( dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY) println "文件夹输出文件路径 " + dest //记得把文件复制到下一个transform使用,不要下一个transform任务拿不到,也就生成不了APK FileUtils.copyDirectory(dirInput.file, dest) } //1.3 在从jar包拿到需要的处理的class文件(注意:如果工程没有jar包,一般不需要从这里取) allInput.jarInputs.each { jarInput -> //1.4 获取输出的文件夹 def dest = transformInvocation.outputProvider.getContentLocation( jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR) println "Jar包输出文件路径 " + dest //把文件复制到下一个transform使用 FileUtils.copyFile(jarInput.file, dest) } } } /** *找到class结尾的文件 * @param dir * @param fileNamePath >>app\build\intermediates\javac\release\classes */ private void findTarget(File dir, String fileNamePath) { if (dir.isDirectory()) { dir.listFiles().each { findTarget(it, fileNamePath) } }else { def filePath = dir.absolutePath if (filePath.endsWith(".class")) { println "找到Class"+filePath //修改文件 modify(filePath, fileNamePath) } } } private void modify(String filePath, String fileNamePath) { //过滤没用的文件 if (filePath.contains('R$') || filePath.contains('R.class') || filePath.contains("BuildConfig.class")) { return } println "开始修改Class"+filePath //因为Javassist需要class包名也就是》》com.example.javassist.MainActivity def className = filePath.replace(fileNamePath, "") .replace("\\", ".") .replace("/", ".") def name = className.replace(".class", "").substring(1) println "包名为:" + name //把class添加到pool中,才能修改class文件 project.android.bootClasspath.each { pool.appendClassPath(it.absolutePath) } CtClass ctClass= pool.get(name) //添加插入代码 addCode(ctClass, fileNamePath) } private void addCode(CtClass ctClass ,String fileName) { //使class变成可修改 ctClass.defrost() //获取class所有的方法 CtMethod[] methods = ctClass.getDeclaredMethods() for (method in methods) { println "method "+method.getName()+" 参数个数 "+method.getParameterTypes().length if (method.getName().matches("hello")){ method.addLocalVariable("start",CtClass.longType); method.insertBefore("{ start = System.currentTimeMillis();}"); method.insertAfter("{ " + " long last = System.currentTimeMillis() - start;"+ "System.out.println(\" 方法耗时:\"+last);" + "}"); } } for (method in methods){ println "deleteCodeInMethod start method"+method deleteCodeInMethod(method) } //把修改的内容写入文件 ctClass.writeFile(fileName) //释放内存 ctClass.detach() } private void deleteCodeInMethod(CtMethod method){ method.instrument(new ExprEditor(){ @Override void edit(MethodCall m) throws CannotCompileException { println("getClassName: "+ m.getClassName()+ " getMethodName: "+m.getMethodName() + " line: " + m.getLineNumber()); if (m.getClassName().matches(".*Log") && m.getMethodName().matches("e")){ println "modify>>>>>" m.replace("{\$_;}") } } }) }}
Transform注册
class MyPlugin implements Plugin{ @Override void apply(Project project) { println "this is a myplugin" project.extensions.getByType(BaseExtension.class) .registerTransform(new MyTransform(project)) }}
2.3 核心代码
代码插入
循环Class中所有的方法,匹配到我们需要修改的方法,然后对方法进行代码插入
for (method in methods) { println "method "+method.getName()+" 参数个数 "+method.getParameterTypes().length if (method.getName().matches("hello")){ method.addLocalVariable("start",CtClass.longType); method.insertBefore("{ start = System.currentTimeMillis();}"); method.insertAfter("{ " + " long last = System.currentTimeMillis() - start;"+ "System.out.println(\" 方法耗时:\"+last);" + "}"); } }
代码修改
循环Class中所有的方法,匹配到我们需要修改的方法,然后对其方法的Body进行扫描,MethodCall 就是方法里每一行代码执行的回调,匹配出我们需要修改的代码,进行删除。
private void deleteCodeInMethod(CtMethod method){ method.instrument(new ExprEditor(){ @Override void edit(MethodCall m) throws CannotCompileException { println("getClassName: "+ m.getClassName()+ " getMethodName: "+m.getMethodName() + " line: " + m.getLineNumber()); if (m.getClassName().matches(".*Log") && m.getMethodName().matches("e")){ println "modify>>>>>" m.replace("{\$_;}") } } }) }
代码地址