Androidgradleがktsに移行されました

バックグラウンド

サーバー、Android、クロスプラットフォームKmm、kotlinネイティブなど、kotlin言語がさまざまな分野に浸透している環境では、ほとんどすべてのフィールドをkotlinで記述できます。もちろん、まだ未成熟な場所がありますが、JBの目標は非常に一貫しています!最も一般的に使用されているgradleビルドツールもkotlinを長い間サポートしてきましたが、コンパイル速度や変換コストのため、実際にkts変換を実装するプロジェクトはほとんどありません。著者のmacm1では、最新バージョンのASを使用してbuild.gradle.ktsをコンパイルします。速度は、groovyで記述されたgradleスクリプトに匹敵するため、記録を作成して共有したいと考えて、この記事を作成します。

グルーヴィー kotlin
利点:より高速な構造、広く使用されている、動的で柔軟な 利点:すべてがコンパイル時に行われ、構文が簡潔であり、一連の言語を使用して、Androidプロジェクトでビルドスクリプトとアプリの作成を開発できます
短所:糖衣構文の作用下では、gradle操作の全体像を理解するのが難しく、機能が単一であり、メンテナンスコストが高くなります。 短所:コンパイルは、グルーヴィーで学習教材が少ないよりもわずかに遅くなります

主流のgradleスクリプトはまだグルーヴィーですが、Android開発者の公式ウェブサイトでもkotlinへの移行を推奨しています

コンパイル前の準備

ここでこの記事を読むことをお勧めします。これには多くの乾物も含まれていますが、

''を""にグローバルに置き換えます

kotlinでは、文字列は ""で表されますが、これはgroovyの''とは異なるため、グローバルに置き換える必要があります。ショートカットコマンド/control+ shift + Rを使用してグローバルに置き換えることができ、一致する正規表現を選択して、ファイルマスクを*.gradleに設定します。

正则表达式
'(.*?[^\\])'
作用范围为
"$1"

image.png

グローバル置換メソッド呼び出し

groovyでは、メソッドは次のようになります。

apply plugin: "com.android.application"

これは実際にはapplyメソッドを呼び出しており、名前付きパラメーターはpluginであり、コンテンツは「com.android.application」です。ただし、kotlin構文では、()またはinvokeによってメソッドを呼び出す必要があるため、次のようになります。すべてのGroovy関数呼び出しにadd()を与える

正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)

image.png 很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为

task(sourcesJar(type: Jar) {
    from(android.sourceSets.main.java.srcDirs)
    classifier = "sources"
})

gradle kt化

接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。

开始前准备

以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如

image.png 对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用

ext

我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!

val myNewProperty by extra("initial value")

但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!

task

  • 新建task
groovy版本

task clean(type: Delete) {
    delete rootProject.buildDir
}

比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称

task<Delete>("clean",{
    delete(rootProject.buildDir)
})

当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过

 ./gradlew help --task task名

去查看相应的类型

  • 已有task修改

对于有些是已经在gradle编译时存在的函数任务,比如

groovy版本

wrapper{
       gradleVersion = "7.1.1"
       distributionType = Wrapper.DistributionType.BIN
   }

这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到

tasks {
    named<Wrapper>("wrapper") {
            gradleVersion = "7.1.1"
            distributionType = Wrapper.DistributionType.BIN

    }
}

这种方式,去找到一个我们想要的task,并配置其内容

  • 生命周期函数

我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast

tasks.create("greeting") {
    doLast { println("Hello, World!") }
}

再比如dependOn

task<Jar>("javadocJar", {
    dependsOn(tasks.findByName("javadoc"))
})

动态函数

sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员

sourceSets{
    val main by getting{
        jniLibs.srcDirs("src/main/libs")
        jni.srcDirs()
    }

}

也可以通过getByName方式去获取

sourceSets.getByName("main")

plugins

在比较旧的版本中,我们AS默认创建引入一个plugin的方式是

apply plugin: 'com.android.application'

其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成

plugins {
    id("com.android.application")
}

就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。

说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!

Plugins解析

我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看

DefaultScriptPluginFactory

            final ScriptTarget initialPassScriptTarget = initialPassTarget(target);

            ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);

            // 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
            CompileOperation<?> initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
            Class<? extends BasicScript> scriptType = initialPassScriptTarget.getScriptClass();
            ScriptRunner<? extends BasicScript, ?> initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
            initialRunner.run(target, services);

            PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
            PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

            PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
            pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

            // 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
            final ScriptTarget scriptTarget = secondPassTarget(target);
            scriptType = scriptTarget.getScriptClass();

            CompileOperation<BuildScriptData> operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);

            final ScriptRunner<? extends BasicScript, BuildScriptData> runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
            if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
                scriptTarget.attachScript(runner.getScript());
            }
            if (!runner.getRunDoesSomething()) {
                return;
            }

            Runnable buildScriptRunner = () -> runner.run(target, services);

            boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
            scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
        }

       
 

可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。

image.png

那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码

重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段

public class DefaultCompileOperationFactory implements CompileOperationFactory {
    private static final StringInterner INTERNER = new StringInterner();
    private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
    private static final String BODY_COMPILE_STAGE = "BODY";

    private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
    private final DocumentationRegistry documentationRegistry;

    public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
        this.documentationRegistry = documentationRegistry;
    }

    public CompileOperation<?> getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
        InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
        SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
        String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
        return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
    }

    public CompileOperation<BuildScriptData> getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
        BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
        String operationId = scriptTarget.getId();
        return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
    }
}

getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行


    @Override
    public Statement transform(SourceUnit sourceUnit, Statement statement) {
        ...

        if (scriptBlock.getName().equals(PLUGINS)) {
            return transformPluginsBlock(scriptBlock, sourceUnit, statement);
        }
        ...
        

总结

文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!

引用&参考

docs.gradle.org/current/use…

www.bennyhuo.com/2021/04/18/…

ナゲッツテクノロジーコミュニティのクリエイター署名プログラムの募集に参加しています。リンクをクリックして登録し、送信してください。

おすすめ

転載: juejin.im/post/7116333902435893261