Android中Gradle插件和Transform

作者: 小红军storm | 来源:发表于2019-03-03 13:18 被阅读13次

    目录
    1、Gradle插件
    2、Transform
    3、ASM

    1、Gradle插件

    1.1、Gradle插件是什么?

    Gradle插件打包了可重用的构建逻辑,可以适用不同的项目和构建。

    1.2、自定义Gradle插件的流程

    (1)、新建一个 Android Library项目,然后除了主目录删除主目录中的所有文件;
    (2)、main目录下建立groovy目录和 resources目录,groovy目录用于写插件逻辑, resources目录下用于声明自定义的插件;
    (3)、书写插件的方法就是,写一个类实现Plugin类,并实现其apply方法,在apply方法中完成插件逻辑;
    (4)、在resources目录下(建立/META-INF/gradle-plugins目录,并)建立一个(plugin.)properties的文件,在里面声明自定义的插件。这个properties文件的名称是我们应用插件时使用的名称。

    1.3、Gradle插件应用流程

    (5)、使用uploadArchives将插件上传的maven库。
    (6)、依赖路径,使用apply plugin应用插件。

    2、Transform API

    2.1、Transform API是什么

    Transform用于在编译打包的.class文件到.dex文件流程中,去转换.class文件。
    目前 jarMerge、proguard、multi-dex、Instant-Run都已经换成 Transform 实现。

    2.2、如何注册一个自定的Transform

    public class SingleClickHunterPlugin implements Plugin<Project> {
        @Override
        public void apply(Project project) {
            AppExtension appExtension = project.getExtensions().getByType(AppExtension);
            appExtension.registerTransform(new SingleClickHunterTransform(project), Collections.EMPTY_LIST);
        }
    }
    

    在自定义插件的apply方法中,获取module对应的project的AppExtension,然后通过其registerTransform方法注册一个自定义的Transform。

    注册之后,在编译流程中会通过TaskManager#createPostCompilationTasks为这个自定义的Transform生成一个对应的Task,(transformClassesWithSingleClickHunterTransformForDebug),在.class文件转换成.dex文件的流程中会执行这个Task,对所有的.class文件(可包括第三方库的.class)进行转换,转换的逻辑定义在Transform的transform方法中。

    2.3、自定义一个Transform

    public class CustomTransform extends Transform {
        @Override
        public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation);
            //当前是否是增量编译(由isIncremental() 方法的返回和当前编译是否有增量基础)
            boolean isIncremental = transformInvocation.isIncremental();
            //消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
            Collection<TransformInput> inputs = transformInvocation.getInputs();
            //OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
            TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    
            for(TransformInput input : inputs) {
                for(JarInput jarInput : input.getJarInputs()) {
                    File dest = outputProvider.getContentLocation(
                            jarInput.getFile().getAbsolutePath(),
                            jarInput.getContentTypes(),
                            jarInput.getScopes(),
                            Format.JAR);
                    //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了        
                    FileUtils.copyFile(jarInput.getFile(), dest);
                }
    
                for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
                    File dest = outputProvider.getContentLocation(directoryInput.getName(),
                            directoryInput.getContentTypes(), directoryInput.getScopes(),
                            Format.DIRECTORY);
                    //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了        
                    FileUtils.copyDirectory(directoryInput.getFile(), dest);
                }
            }
        }
        @Override
        public String getName() {
            return "CustomTransform";
        }
        @Override 
        public boolean isIncremental() {
            return true; //是否开启增量编译
        }
        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS;
        }
        @Override
        public Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT;
        }
    }
    

    在transform方法中,我们需要将每个jar包和class文件复制到dest路径,这个dest路径就是下一个Transform的输入数据。而在复制时,就可以将jar包和class文件的字节码做一些修改,再进行复制。

    2.4、Transform两个过滤纬度

    Transform两个过滤纬度

    ContentType,数据类型,有CLASSES和RESOURCES两种。
    其中的CLASSES包含了源项目中的.class文件和第三方库中的.class文件。
    RESOURCES仅包含源项目中的.class文件。
    对应getInputTypes() 方法。

    Scope,表示要处理的.class文件的范围,主要有
    PROJECT, SUB_PROJECTS,EXTERNAL_LIBRARIES等。
    对应getScopes() 方法。

    2.5、支持增量编译

    Transform支持增量编译分为两步:

    (1)重写Transform的接口方法:isIncremental(),返回true。

    @Override 
    public boolean isIncremental() {
        return true;
    }
    

    (2)判断当前编译对于Transform是否是增量编译:
    如果不是增量编译,则清空output目录,然后按照前面的方式,逐个class/jar处理;
    如果是增量编译,根据每个文件的Status,处理文件:
    NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
    ADDED、CHANGED: 正常处理,输出给下一个任务;
    REMOVED: 移除outputProvider获取路径对应的文件。

    注意:当前编译对于Transform是否是增量编译受两个方面的影响:
    (1)isIncremental() 方法的返回值;
    (2)当前编译是否有增量基础;(clean之后的第一次编译没有增量基础,之后的编译有增量基础)

    增量的速度比全量的速度提升了3倍多,而且这个速度优化会随着工程的变大而更加显著。

    2.6、支持并发编译

    private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
    //异步并发处理jar/class
    waitableExecutor.execute(() -> {
        bytecodeWeaver.weaveJar(srcJar, destJar);
        return null;
    });
    waitableExecutor.execute(() -> {
        bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
        return null;
    });  
    //等待所有任务结束
    waitableExecutor.waitForTasksWithQuickFail(true);
    

    为什么要等待所有任务结束?
    如果不等待,主线程就会进入下一个任务的处理,可能当前的任务的处理工作还没完成。

    并发Transform和非并发Transform下,编译速度提高了80%。

    3、ASM

    ASM ,速度快、代码量小、功能强大,要写字节码、学习曲线高。
    Javassist,学习简单,不用写字节码,比ASM慢,功能少。

    3.1、ASM访问字节码流程

    private void copy(String inputPath, String outputPath) {
            FileInputStream is = new FileInputStream(inputPath);
            ClassReader cr = new ClassReader(is);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            cr.accept(cw, 0);
            FileOutputStream fos = new FileOutputStream(outputPath);
            fos.write(cw.toByteArray());
    }
    

    (1)、ClassReader负责读取.class字节码;
    (2)、ClassReader将所有字节码传递ClassWriter(是一个ClassVisitor)中的(多个)visitxxx接口方法依次进行处理;
    (3)、ClassWriter访问某个方法时会将这个方法的所有字节码传递给MethodWriter(是一个MethodVisitor)处理。

    默认ClassWriter会保存传递到它的所有字节码,可使用ClassWriter.toByteArray()方法获取经过ClassWriter的字节码。

    3.2、以上流程代码证明:ClassReader.accept()。

    public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int parsingOptions) {
        
        // 读取当前class的字节码信息
        int accessFlags = this.readUnsignedShort(currentOffset);
        String thisClass = this.readClass(currentOffset + 2, charBuffer);
        String superClass = this.readClass(currentOffset + 4, charBuffer);
        String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)];
        
        //classVisitor就是刚才accept方法传进来的ClassWriter,每次visitXXX都负责将字节码的信息存储起来
        classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
        
        /**
            略去很多visit逻辑
        */
        //visit Attribute
        while(attributes != null) {
            Attribute nextAttribute = attributes.nextAttribute;
            attributes.nextAttribute = null;
            classVisitor.visitAttribute(attributes);
            attributes = nextAttribute;
        }
        /**
            略去很多visit逻辑
        */
        classVisitor.visitEnd();
    }
    

    Gradle中的ClassWriter默认对传递给它的字节码不做任何处理,只做保存工作。
    通过默认ClassWriter处理字节码的流程如下:


    通过默认ClassWriter处理字节码的流程

    3.3、修改字节码

    要修改字节码,需要自定义ClassWriter,在其访问类的相应方法时对其做相应操作(使用自定义的MetiodWriter),达到字节码插桩的目的。

    修改字节码

    3.4、什么事增量编译

    我理解的增量编译:
    1、基于Task的上次输出快照和这次输入快照对比,如果相同,则跳过相应任务;
    2、基于Task本身是否支持增量更新。

    3.4、增量编译实验

    3.4.1、Transform 的isIncremental()返回true。
    @Override
    public boolean isIncremental() {
        return true;
    }
    

    (1)、clean之后,第一次编译,即使Transform里面isIncremental()返回true,Transform开启了增量编译,此时对Transform来说仍然不是增量编译, transform方法中isIncremental = false;

    (2)、不做任何改变直接进行第二次编译,Transform别标记为up-to-date,被跳过执行;

    (3)、修改一个文件中代码,进行第三次编译,此时对Transform来说是增量编译,transform方法中isIncremental = false。

    3.4.2、Transform 的isIncremental()返回false。
    @Override
    public boolean isIncremental() {
        return false;
    }
    

    (1)、clean之后,第一次编译,此时对Transform来说不是增量编译, transform方法中isIncremental = false;

    (2)、不做任何改变直接进行第二次编译,Transform别标记为up-to-date,被跳过执行;

    (3)、修改一个文件中代码,进行第三次编译,此时对Transform来说不是增量编译,transform方法中isIncremental = false。

    结论:1、一次编译对Transform来说是否是增量编译取决于两个方面:
    (1)、当前编译是否有增量基础;
    (2)、当前Transform是否开启增量编译。

    结论:2、不管Transform是否开启增量编译,若TransformTask的当前输入快照和上次输出快照相同,则跳过当前TransformTask。

    4、Gradle插件和Transform实战应用

    防止快速点击的小插件

    https://github.com/Leaking/Hunter/pulls

    相关文章

      网友评论

        本文标题:Android中Gradle插件和Transform

        本文链接:https://www.haomeiwen.com/subject/bdmpuqtx.html