Android Gradle——自定义Transform,使用Javassist工具插入和删除代码

一、前言

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

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

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


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

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

二、工程准备

2.1 基础知识预备

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

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

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

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<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }
 
    //你想要处理的范围
    @Override
    public Set<? super QualifiedContent.Scope> 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<Project>{
    @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("{\$_;}")
                }
            }
        })
    }

 代码地址:JavassistDemo: 用Javassist动态插入代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值