kts로 마이그레이션된 Android gradle

배경

코틀린 언어가 서버, 안드로이드, 크로스 플랫폼 Kmm, 코틀린 전용 등 다양한 분야에 침투한 환경에서는 거의 모든 필드를 코틀린으로 작성할 수 있지만, 물론 아직 미숙한 부분도 있지만 JB의 목표는 매우 일관적입니다! 우리가 가장 많이 사용하는 gradle 빌드 도구도 오랫동안 kotlin을 지원했지만 컴파일 속도나 변환 비용으로 인해 실제로 kts 변환을 구현하는 프로젝트는 거의 없습니다. 저자의 mac m1에서는 최신 버전의 AS를 사용하여 build.gradle.kts를 컴파일하고 있는데 groovy로 작성한 gradle 스크립트와 속도가 비슷해서 기록을 남기고 공유하고자 이 글을 쓰게 되었습니다.

그루비 코틀린
장점: 더 빠른 구성, 널리 사용됨, 동적 및 유연성 이점: 모든 것이 컴파일 시간에 완료되고 구문이 간결하며 Android 프로젝트에서 빌드 스크립트 및 앱 작성을 개발하는 데 언어 세트를 사용할 수 있습니다.
단점: 구문 설탕의 작용으로 gradle 동작의 전체 그림을 이해하기 어렵고 기능이 단일하며 유지 보수 비용이 높습니다. 단점: 컴파일이 그루비보다 약간 느리고 학습 자료가 적습니다.

주류 Gradle 스크립팅은 여전히 ​​그루브하지만 Android 개발자의 공식 웹 사이트에서도 kotlin으로의 마이그레이션을 권장합니다.

컴파일 전 준비

건조 제품도 많이 다루는 이 기사 를 여기에서 읽는 것이 좋습니다 .

전역적으로 ''를 ""로 바꿉니다.

kotlin에서 string은 groovy의 ' '와 달리 ""로 표현되므로 전역적으로 바꿔야 합니다. 단축키 command/control+shift+R을 통해 전역적으로 교체하고 일치하는 정규식을 선택하고 파일 마스크를 *.gradle로 설정할 수 있습니다.

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

이미지.png

전역 대체 메서드 호출

groovy에서 메서드는 can hide()입니다. 예를 들어

apply plugin: "com.android.application"

이것은 실제로 apply 메소드를 호출하는 것이고 이름이 지정된 매개변수는 plugin이고 내용은 "com.android.application"이지만 kotlin 구문에서는 () 또는 invoke로 메소드를 호출해야 하므로 모든 groovy 함수 호출에 add()

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

이미지.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却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如

이미지.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之后才真正生效的。

이미지.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/…

Nuggets Technology Community의 작성자 서명 프로그램 모집에 참여하고 있습니다. 링크를 클릭하여 등록하고 제출 하십시오.

Supongo que te gusta

Origin juejin.im/post/7116333902435893261
Recomendado
Clasificación