您当前所在的位置:官网首页 > 新闻资讯 > 行业新闻 >

如何开发一款高性能的 gradle transform

这里我们要在自定义的Plugin中注册

class MethodTracePlugin implements Plugin Project  {    @Override    void apply {         project.getExtensions                .create         ……    }}

注册后,我们可以在引入了这个插件的build文件中做出如下配置

apply plugin: 'com.jianglei.method-tracer'……methodTrace{    traceThirdLibrary = false}

获取配置很简单,只要用如下代码就可以了:

        //获取配置信息        MethodTraceExtension extension = project.getExtensions.findByType

问题是你什么时候获取这个配置信息呢?刚开始,我在注册这个配置后直接去获取:

 @Override    void apply {          //注册配置         project.getExtensions                .create         //获取配置信息        MethodTraceExtension extension = project.getExtensions.findByType         ……    }

我希望在这里获取配置然后传入到Transform中去,事实上这是不可取的,此处的apply方法被调用时机是apply plugin代码被调用的时候,此时,我们在build.gradle中的配置代码快还没有被调用,所以是取不到我们想要的配置的,取到的都是默认值。

那么到底我们应该怎么获取呢?其实我们只要在transform方法中获取就可以了,这个时候build.gradle中配置的代码已经执行过了:

    @Override    void transform throws TransformException, InterruptedException, IOException {        //获取配置信息        MethodTraceExtension extension = project.getExtensions.findByType         ……    }

现在,我们有了一个可配置的插件去修改所有的class文件了,功能上的需求我们已经完成了,但是,性能上够了吗?

目前,我们的插件都是直接在application模块中引入的,那么多模块情况下怎么办?每个模块都要引入吗?可以只在主模块引入吗?应该只在主模块引入吗?

我们知道,butterknife是需要在每个模块都引入的,其实,对于多模块来说,我们完全可以只在application主模块中引入插件,这里要注意Transform中的getScopes方法:

    @Override    Set ? super QualifiedContent.Scope  getScopes {        //此次是只允许在主module        //所以我们需要修改所有的module        return TransformManager.SCOPE_FULL_PROJECT    }

这里的SCOPE_FULL_PROJECT其实是这样的:

        SCOPE_FULL_PROJECT = Sets.immutableEnumSet;

说明这里处理的模块包括本模块,子模块以及第三方jar包,这样我们就能在主模块中处理所有的class文件了,可见我们是可以只在主模块中引入的,这样做的话,所有子模块会以jar包的形式作为输入。

那么如果想要在每个module中都引入该如何做呢?

首先是注册方式要修改:

    @Override    void apply {        project.getExtensions                .create        def extension = project.getExtensions.findByType        def isForApplication = true        if  {            //说明当前使用在library中            extension = project.getExtensions.findByType            isForApplication = false        }        extension.registerTransform)    }

关键是我们在Transform中要记录当前是应用于主模块还是子模块了。这种模式下,每一个模块都会执行自己的transform方法,所以这里的getScopes方法要做些修改:

 @Override    Set ? super QualifiedContent.Scope  getScopes {        def scopes = new HashSet        scopes.add        if  {            //application module中加入此项可以处理第三方jar包            scopes.add        }        return scopes    }

这里对于主模块的情况下应该额外处理第三方jar包,子模块只要处理自己的项目代码即可。

其实进过实验,所有子模块的依赖的第三方jar包只会在处理主模块中输入,换句话说子模块是永远不可能处理第三方jar包的。

两种方式都是可以的,那么到底该选那种呢?有什么选择依据吗?从上面的介绍来看没有,而且应用所有子module的方式编写起来似乎还要复杂一点,那是不是应该选择只在主模块引入插件呢?其实不然,最大的区别下面会讲到,到时候自然有结果。

通过上面的介绍,完成一个插件已经不是问题,但是这里有一个问题,每次编译时,transform方法都会执行,我们会遍历所有的class文件,会解压缩所有jar文件,然后重新压缩成所有jar文件,但事实上,一次编译有可能只改动了一个class文件,我们能不能做到只重新修改这一个class文件呢?gradle其实是提供了方法的。

transform-api将输入文件分成了两类:

DirectoryInput,包装的是源码对应的class文件,长这样:

public interface DirectoryInput extends QualifiedContent {    Map File, Status  getChangedFiles;}

换句话说,我们可以通过以下方式获取改动的class文件:

 input.directoryInputs.each { directoryInput -     directoryInput.changedFiles.each{changeFileEntry-         def status = changeFileEntry.value;    } }

这样我们可以遍历所有改动的文件,而且可以获取每个改动文件的状态,有4种:

public enum Status {    NOTCHANGED,    ADDED,    CHANGED,    REMOVED;    private Status {    }}

第一次编译或clean后重新编译directory.changedFiles为空,需要做好区分经测试,删除一个java文件,对应的class文件输入不会出现REMOVED状态,也就是不能从changeFiles里面获取被删除的文件

JarInput  和DirectoryInput不同,JarInput只能获取状态,也有4种状态:

public interface JarInput extends QualifiedContent {    Status getStatus;}

也就是说,我们如果想要增量编译,应该处理所有非 Status.NOTCHANGED状态的jar包,同样如果移除了一个依赖,这个jar包就再也不会输入,自然也就不会出现Status.REMOVED状态的jar包了。

有了以上对gradle transform增量机制的了解,相信大家都对如何支持增量编译有了一个基本的了解,但是想要开发一个健壮的、支持增量的插件还有很多问题要解决,我们一一探讨。

4.2.2.1 如何区分未编译和未修改

之前提到,对于DirectoryInput来说,未编译或clean后重新编译时Directory.changedFiles为空,未修改时也是为空,前一种状态下我们需要处理所有的文件,后一种状态下又不应该处理任何文件,同样,JarInput也面对这个问题,要解决也很简单,这里给出一种简单方案,首次编译时生成一个标记文件,下一次编译时,如果修改文件为空,我们判断该标记文件是否存在,存在就是未修改,否则就是首次或clean后重新编译。当然上次编译也会有文件输出,我们可以直接拿任一输出文件做这个标记文件。

4.2.2.2 如何解决增量编译时包重复问题一般来说,如果我们依赖了一个第三方jar包,比如:

    implementation "commons-io:commons-io:2.4"

首次编译会在编译输出目录下生成一个文件,比如:

/home/jianglei/AndroidStudioProjects/ASMStudy/app/build/intermediates/transforms/MethodTrace/debug/32.jar

现在我们注释掉这个包的引入,重新编译,之前我们提过,删除了一个jar包引用后我们是收不到任何信息的,无法对这个包做任何处理,因为它根本就不会被输入,那么自然这个32.jar还在那里,这个时候我们在重新引入刚才被移除的依赖,这个时候生成的文件变成了:

/home/jianglei/AndroidStudioProjects/ASMStudy/app/build/intermediates/transforms/MethodTrace/debug/33.jar

这个时候问题就来了,32.jar和33.jar其实是一个jar包,编译时自然会出现类冲突,而且这个冲突还比较尴尬,不好排查,因为gradle文件是没有任何问题的,最简单的方法就是clean后重新编译,这个问题自然不存在了,但一般开发者是没有这个意识的,这样做也太麻烦了,删掉一个依赖再重新引入是很正常操作,为什么非要先clean呢?

现在,我们来看下解决方案:解决思路很简单,要是我们能够找到此次编译时哪些jar包被删除了,我们自己手动删除该jar包上次编译的输出文件不就解决了冲突问题吗?所以我们完全可以自己记录下每次编译时有哪些jar包参与了编译,并且输出到了哪里,如下:

{        "commons-io:commons-io:2.4": "/home/jianglei/AndroidStudioProjects/ASMStudy/app/build/intermediates/transforms/MethodTrace/debug/5.jar",        ……    }

那么此次编译时,我们读取上次的文件,对比两次参与编译的jar包,如果有删除的,我们自己删除该jar包对应的输出文件即可。

4.2.2.4 如何判断配置文件改变和上面的class文件改变或jar改变都不一样,配置文件改变transform是得不到任何额外的信息的,但你不能不处理,比如说上次配置文件定义如下:

methodTrace{    traceThirdLibrary = false}

编译后自然不会处理第三方的jar包,但现在将其改成了false , 这个时候,上次编译的所有结果都要重来,因为这次需要处理第三方jar包了。解决方案也很简单,既然gradle没有通知我们配置文件改变了,我们自己记录上次配置文件,和本次编译对比,如果配置文件改变就全部重来,这个时候记录的文件就变成了这样:

{  "extension": {    "traceThirdLibrary": true  },  "jarMap": {     "commons-io:commons-io:2.4": "/home/jianglei/AndroidStudioProjects/ASMStudy/app/build/intermediates/transforms/MethodTrace/debug/5.jar",      ……   }}

这里有一个问题没有解决,即使是自己对比配置文件是否改变 ,这些代码都是写在transform方法中的,如果此次编译只是修改配置文件,没有修改任何东西,gradle认为你什么都没有改动,直接不调用transform方法了,这个就意味着你给了配置文件增量编译不生效,暂时没有好的解决方案,只能重新CLEAN,或者修改其他的java文件,都能触发重新编译。如果大家有更好的解决方案,希望能指出来。

之前我们还有一个问题没有解决,那就是gradle插件到底是应该只在主module中引入还是再所有的module中都引入。在我看来,衡量的关键点就是编译速度,如果只在主module中引入的话,子module其实是以jar包的形式作为输入文件来 处理的,这样我们就算只修改了子module中一个文件,我们都需要将整个jar解压,然后处理该jar中的所有class文件,最后还得压缩一次,多做了无用功;如果我们放在所有的module中引入的话,针对这种情况我们只需要处理改动的class文件即可,能节省很多时间,所以我推荐放到所有module引入。

有了上面的知识,我相信大家应该都能开发出一个健壮的、支持增量编译的插件了,然后你就能利用字节码插桩技术____了,上面这些源码大家可以点这里:https://github.com/FamliarMan/ASMStudy , 当然这些都是我自己瞎琢磨出来的,网上似乎没有查到相关资料,如果有错误,恳请指正,不甚感激!

推荐阅读Gradle 的高级技巧

扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

上一篇:有赞移动 iOS 组件化(模块化)架构设计实践 下一篇:没有了