> 文档中心 > Android Gradle(九)自定义Transform插入和删除代码

Android Gradle(九)自定义Transform插入和删除代码


一、前言

        在安卓中,难免会碰到需要动态插入代码,或者删除代码。这时候就需要用到自定义Transform任务,来对Class文件进行扫描和获取。

  • 可以插入的代码场景有哪些?例如:打印方法的执行时间等。
  • 可以删除的代码场景有哪些?例如:把代码中Log.e日志打印的代码去掉等

先看下代码前后效果,原始代码如下:

经过修改后(通过反编译apk得到源码):

接下来,让我们看看如何对以上两个场景进行代码的插入和删除。

二、工程准备

2.1 基础知识预备

     1.需要用到Javassist来修改代码,不会的可以看看如何使用

      2.需要用到自定义插件

      如果你已经有了上面两个基础后,那我们就可以开始搭建工程了。

2.2 创建Transform

创建一个Transform用来对代码进行处理

流程如下:

  1. 从Transform中拿到我们需要处理的Class文件路径
  2. 从Class文件路径中找到Class文件结尾的文件名(因为可能有)
  3. 找到需要的Class文件对其代码进行插入和修改
  4. 除了代码的插入和修改,其他基本都是模板代码,不要纠结。😀
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("{\$_;}")  }     } })    }

 代码地址