kts脚本加载流程

和groovy脚本一样,kts脚本也分为2个阶段

  • stage 1
    执行buildscript和plugins部分,执行结果会对stage2 program的classpath有影响

  • stage 2
    eval脚本剩余的部分

2个阶段都会生成Program子类,目录在$HOME/.gradle/caches/gradle-version/kotlin-dsl/scripts/hashcode/classes

和groovy语言本身提供了丰富的动态能力不同,gradle对kts的解析有大部分工作是自己完成的,涉及到编程原理部分,比如从lex阶段开始对kts内的token做解析等

整体流程如下图所示

如需全套 Gradle深度解析 学习资料 请点击免费领取

Lexer

Lexer接收脚本文件内容及脚本对应的TopLevelBlockIds(buildscript/initscript/pluginManagement/plugins)这些,返回经过lex处理过的Packaged<LexedScript> ,里面包含注释及提取的第一轮需要执行的顶层block

fun lex(script: String, vararg topLevelBlockIds: TopLevelBlockId): Packaged<LexedScript>class Packaged<T>(  val packageName: String?,  val document: T
)class LexedScript(  val comments: List<IntRange>,  val topLevelBlocks: List<TopLevelBlock>
)

其是通过org.jetbrains.kotlin.lexer.KotlinLexer的能力进行解析的,

start()
advance()
tokenType
tokenText
tokenStart
tokenEnd

start接收文本内容,开始进行词法分析,编译的第一步就是进行词法分析,程序的内容会被全部解析为token advance跳至下一个token部分 tokenType token的类型, 例如空格为KtTokens.WHITE_SPACE 注释为KtTokens.COMMENTS(这是个集合,包含了EOL_COMMENT-行尾注释,BLOCK_COMMENT-块注释等) LBRACE -> {左花括号 RBRACE -> }右花括号 IDENTIFIER,这个涵盖的范围特别广,以下面的statement为例 val a = buildscript() val等关键字,变量如a、b,=等号,方法名buildscript等都属于此

tokenText token的内容,分析IDENTIFIER特别需要,还是上面的statement为例,val的tokenType是IDENTIFIER,它的tokenText为val tokenStarttokenEnd token的起始,结束位置

gradle kts的Lexer目的是为了将顶层的特定block解析出来,这些block比较特殊,是需要优先执行的。解析的结果就是包含哪些特殊的block以及它们的起始结束位置

Lexer靠不断的迭代tokenType来解析脚步内容,遇到WHITE_SPACE或者注释类型的token就跳过,它以state记录当前所处状态,state有3种类型

  • SearchingTopLevelBlock
  • SearchingBlockStart
  • SearchingBlockEnd

默认处于搜索SearchingTopLevelBlock状态,在这个状态下如果碰到了PACKAGE,会把包名解析出来并保存,如果碰到IDENTIFIER,则判断是否有符合的toplevelblock,且当前depth为0,当前depth是对花括号的记录,碰到花括号depth会+1,出花括号-1,因为这里是要收集顶层的block,所以进入花括号属于内层的需要忽略

SearchingTopLevelBlock状态下检测到符合条件的block时会进入SearchingBlockStart,很容易看出它和SearchingBlockEnd是一对,目的就是为了将block的闭包起始结束位置记录下来,start就是在找 { 的位置,找到后就进入, start状态下只要不是IDENTIFIER或者LBRACE的情况,都会重置回SearchingTopLevelBlock默认状态重新开始查找
SearchingBlockEnd状态,因为内部还可能有闭包,所以end里仍需要进入{对depth+1,退出}时-1,当depth为0时将start和end点记录下来

还有个细节,是在start状态遇到IDENTIFIER时,还会检测block是否是符合条件的,如果不符合是会重置状态的,因为可能出现下面这种情况,buildscript可以被引用,当解析到第一个buildscript时会进入start状态,如果start不对IDENTIFIER检查,到第二个buildscript时就会重置状态,而错过buildscript的解析了

val a = buildscript
buildscript {}

ProgramParser

ProgramParser是把lex分析后的Packaged<LexedScript>解析成Packaged<Program>

  1. 检查特定的顶层block每个只出一次(buildscript, plugins等)
  2. 检查特定的顶层block的顺序,pluginManagement一定要在第一个,plugins和buildscript优先级一样不做要求
  3. 将源码中的注释擦除,注意擦出不是删除,而是替换换行之外的字符为WHITESPACE,避免起始结束位置错乱
  4. 将特定的顶层block解析出来,若block内容不为空的话转为对应的Program子类,里面会记录其block的起始结束位置,将它们统一聚集到stage1
  5. 再将特定block的代码擦除,如果有剩余代码的话,则将其包在Program.Script内作为stage2
  6. 若stage1、2都存在,将其包在Program.Staged返回,若只存在一个则只返回单个,若都没有则返回Program.Empty
// 只有stage1的block,且block内没有内容,解析后为Empty
buildscript { }
plugins {// comments
}
// stage1的block是空的,解析后为Program.Script
buildscript { }
println "stage2"

Program
Empty
Script
Staged(Stage1, Script)
Stage1
Buildscript
PluginManagement
Plugins
Stage1Sequence(Buildscript, PluginManagement, Plugins)

PartialEvaluator

PartialEvaluator的目的是为了将stage1的部分先reduce,作为stage2的prelude,stage2部分在运行时再编译运行 它将parser解析后的Program转为ResidualProgram,residual有剩余,残留的意思

ResidualProgram
Static(instructions: List<Instruction>)
Dynamic(prelude: Static, source: ProgramSource)

ResidualProgram有2个子类,Static和Dynamic
Static只包含了instructions的集合
Dynamic是由作为prelude的Static和剩余部分的源码组成的

在parser后其实被划分为7种情况,empty,3种特定顶层block只出现一个的场景,3种中有任意2个或以上出现的场景Stage1Sequence,没有特殊顶层block出现的场景Script,stage1和stage2并存的场景Staged

Empty
PluginManagement
Buildscript
Plugins
Stage1Sequence
Script
Staged

只有Staged的情况才会被evaluate为Dynamic,其中stage1部分作为static,剩余的源码部分为source
其他情况都是Static的
可以看出Static主要是服务于stage1中特殊的顶层block的,比较重要的一些instruction有 SetupEmbeddedKotlin,ApplyPluginRequestsOf,Eval

值得注意的一点是,这里对plugins的evaluate有优化,对其单独进行了lex,将插件id等提取出来

ResidualProgramCompiler

residual有剩余,残留的意思
PartialEvaluator生成的ResidualProgram实际上是一堆粗略的指令,ResidualProgramCompiler就是将这堆指令使用asm技术翻译成字节码

对于Static和Dynamic的处理方式差不多,主要区别在于Dynamic的分为2个阶段,第一个阶段也是static的,生成的第二个阶段代码实际只是将源码先保存在字节码中,等实际执行的时候再去调用Interpreter走整个流程

Static编译为ExecutableProgram的子类
Dynamic编译为ExecutableProgram.StagedProgram的子类
下面为删减的代码,编译后的Program需要实现execute方法,这里只会有部分特定顶层block的执行代码,脚本内其余部分代码是通过kotlin compiler提供的能力编译的。
如果kts脚本被PartialEvaluator reduce 为了Static,那它会被编译为ExecutableProgram的子类,若此时文件内只有plugins时,它在这个execute中就能完成所有工作了,不会通过kotlin compiler生成其他代码,如果不是,那会将其他代码编译为类似Build_gradle的文件,并在execute中对其进行初始化

abstract class ExecutableProgram {  abstract fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<?>)
}abstract class StagedProgram : ExecutableProgram() {abstract val secondStageScriptText: Stringabstract fun loadSecondStageFor(...): CompiledScriptfun loadScriptResource(resourcePath: String): String

当kts脚本被reduce为Dynamic时,这种情况可能更加常见,也更复杂,以下面这个较为简单,除plugins、buildscript外仅有repositories block的脚本为例

plugins {kotlin("jvm") version "1.8.10"
}buildscript {print("test")
}repositories {  mavenCentral()
}

将会生成下面的代码(伪代码)
stage1部分
Program.kt

class Program: ExecutableProgram.StagedProgram() {fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {host.setupEmbeddedKotlinFor(scriptHost)val requestCollector = PluginRequestCollector(scriptHost.getScriptSource())Build_gradle(scriptHost, requestCollector.createSpec(1), scriptHost.getTarget() as Project)host.applyPluginsTo(scriptHost, requestCollector.getPluginRequests())host.applyBasePluginsTo(scriptHost.getTarget() as Project)host.evaluateSecondStageOf(this, scriptHost, "Project/TopLevel/stage2", sourceHash, host.accessorsClassPathFor(scriptHost))}// secondStageScriptText和loadScriptResource都是为了加载stage2的脚本文件内容,因为常量池大小64k的限制,如果超出这个大小才会用loadScriptResource,否则使用字面量fun getSecondStageScriptText(): String {return "repositories { mavenCentral() }"}fun loadSecondStageFor(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>, scriptTemplateId: String, sourceHash: HashCode, accessorsClassPath: ClassPath): CompiledScript {return host.compileSecondStageOf(this, project, scriptTemplateId, sourceHash, ProgramKind.TopLevel, ProgramTarget.Project, accessorsClassPath);}
}

Build_gradle.kt

class Build_gradle(val host: KotlinScriptHost , val pluginDependencies: PluginDependenciesSpec,val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {init {  plugins {  pluginDependencies.kotlin("jvm").version("1.8.10")  }  buildscript {  print("test")  }  }
}

代码比较多,核心部分在于plugins、buildscript这2个顶层block编译在了Build_gradle中,execute中先执行了setupEmbeddedKotlinFor,后面又初始化了Build_gradle,因Build_gradle初始化时就执行了顶层block,后续就是applyPluginsTo去应用plugins引入的插件,最后是对stage2部分的编译及运行
setupEmbeddedKotlinFor是为了统一embededKotlin版本,都是用gradle自带的kotlin版本的,precompile脚本用的kotlin版本和build.gradle.kts保持一致

stage1部分的Build_gradle也不一定会有,这一步和Static生成的逻辑一样,只是多了加载stage2的loadSecondStageFor等部分,下面列了stage2编译后的伪代码,但是实际上此时并没有发生stage2的编译,虽然也是由ResidualProgramCompile来完成的,但stage2部分的编译发生stage1执行时,由Interpreter来触发的

stage2
Program.kt

class Program: ExecutableProgram() {fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {Build_gradle(host, scriptHost.getTarget() as Project)}
}

Build_gradle.kt

class Build_gradle(val host: KotlinScriptHost , val pluginDependencies: PluginDependenciesSpec,val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {init {  project.repositories {mavenCentral()}}
}

从伪代码可以看出stage2部分才执行了repositoriesmavenCentral()方法 stage2的执行是stage1在其execute方法中触发的evaluateSecondStageOf,并且它还调用了accessorsClassPathFor去获取accessors的classpath,accessors在后面会进行详细阐述

Program由ResidualProgramCompiler使用字节码技术生成的,Build_gradle是由kotlin script的KotlinToJVMBytecodeCompiler生成的

KotlinCompiler

gradle kts实际是用kotlin-compiler-embeddable去编译kts脚本的

Kotlin Script最重要的是Script Defination,这是一组用来定义并配置script类型的参数,主要有
baseClass 和groovy的scriptBaseClass类似,作为script的基类
defaultImports 和groovy自带了很多默认导包不同,kts需要自己添加导包
hostConfiguration 添加classpath等

Script Defination有几种定义方法,官方例子中以注解方式配置参数较为常见,gradle中也有注解部分,但更多还是通过代码手动设置的参数

KotlinScript 类似groovy script的org.gradle.api.Script,继承关系如下
)

除此以外还需要配置outputDirectory(生成的class的输出目录),jvm版本,脚本源码路径等等。配置完整体的compiler环境后,调用compileBunchOfSources(environment)即可对kts文件进行编译

Plugins

plugins block在stage1中会被生成为applyPlugins代码,之后和groovy脚本对plugin的加载一样

plugin主要可以分为两类, 一种是precompile script,这又可以细分为buildSrc和includeBuild引入的 一种是external plugin,这是通过设置plugin的repository后apply进来的,也可以细分为两种,gradle官方提供的例如java,kotlin等,另外是用户自定义的

precompile script

这类脚本使用的语言不受限制,只要是JVM的就行,实际上这些脚本会被编译为Plugin的子类,gradle提供了kotlin-dslgroovy-gradle-plugin的插件让我们可以kts或groovy来编写此类脚本,用java也是可以的

kotlin-dsl的实现为KotlinDslPlugin,它会apply PrecompiledScriptPlugins,这个plugin的作用就是Kotlin source-sets下的*.gradle.kts脚本文件输出为Gradle plugin产物,具体是通过DefaultPrecompiledScriptPluginsSupport来实现的

DefaultPrecompiledScriptPluginsSupport

  • ExtractPrecompiledScriptPluginPlugins
    把precompiled script里面的plugins block提取出来,单独输出到outputDir里
  • GenerateExternalPluginSpecBuilders
    从compile classpath中找到带有gradle plugins的jar包,判断依据是其有META-INF/gradle-plugins/*.properties文件,为这些plugin生成accessor,这部分accessor是用在plugins block内的,例如java,kotlin等PluginSpecBuilders
  • CompilePrecompiledScriptPluginPlugins
    ExtractPrecompiledScriptPluginPlugins提取的plugins block和GenerateExternalPluginSpecBuilders生成的PluginSpecBuilders来编译plugins block,这里也是使用KotlinCompiler.compileKotlinScriptModuleTo来处理的,到这一步的处理其实类似stage1的过程
  • GeneratePrecompiledScriptPluginAccessors
    有了前面compile过后的plugins block,就可以来生成type safe accessors了。Plugin提供可以用在脚本里面的有extension(如java,kotlin),task,convention(已经Deprecated了,和extension差不多),containerElement(named方法),configuration(如implementation,api),gradle是通过构建一个虚拟的build,然后对其project apply这些plugin,之后就可以在project对象中获取到上述的extension等信息,提取的代码见DefaultProjectSchemaProvider.schemaFor,收集到的信息被封装在ProjectSchema里,再就是通过org.gradle.kotlin.dsl.accessors.Emitter利用ASM字节码手段生成accessor了。这一步的操作是为了给正式编译kts脚本中使用到的extension等提供accessors源码

在经过上面一系列操作之后,gradle才会开始执行compileKotlin task,这里和编译build.gradle.kts不同,并不是使用KotlinCompiler来完成的,而是通过为freeCompilerArgs属性添加-script-templates-Xscript-resolver-environment这些kts脚本编译参数来完成的,和单纯编译kts脚本不同,src目录下除了kts脚本外还可以有正常的kotlin代码,需要混编。

至此precompiled script编译流程完成,buildSrc和includeBuild在执行的时机上有所不同,buildSrc在root/build.gradle.kts之前,includeBuild实际上是介于root/build.gradle.kts编译过程的stage1与stage2之间,大体上并不影响,因为precompiled script就是为了stage2的编译做准备

apply plugin引入的plugin不是在stage1处理过的,所以没有为其生成accessor类,在build脚本里面是无法使用它的extension的 includeBuild里面的precompiled script因为是在stage1,2之间生成,所以没有对应的PluginSpecBuilders生成,所以只能用id(“”)方法去调用

plugins {java`my-build-src`// buidSrc下的precompiled scriptid("my-include-build")// includBuild的precompiled script
}

Accessors

Accessor是什么

  1. gradle本身提供的一些能力,例如plugins、files、repositories、dependencies
  2. plugin引入的extension,如java,publishing

这些能力是如何能在脚本中被使用到的呢,尤其extension是plugin自己定义的,gradle无法进行约束,不像groovy语言本身提供了动态能力在运行时去派发这些方法调用到具体的extension上去,kotlin是强类型静态语言,在编译时这些类就需要在classpath里面找得到,否则无法compile gradle会生成对应的accessor,来让脚本可以使用到这些能力

举个具体的例子,我们可以在build.gradle.kts脚本里面使用其他插件提供的extension,例如在引入插件java后可以使用java extension对其进行一些配置

// build.gradle.kts
plugins {  id("java")
}java {  toolchain {  languageVersion.set(JavaLanguageVersion.of(11))}
}

我们也可以自定义plugin,并提供自己的extension,例如下面 val proguard = extensions.create<ProguardExtension>("proguard") 定义了proguard extension,这行代码引入了2个东西,一个是extension本身,在这里也就是ProguardExtension,里面可以提供方法来进行配置,另一个是extension的name,这里是proguard,也就是我们引入了这个plugin后,在脚本中可以使用的名字,类似上面java插件中的java extension。

CompilationClassPath

分为2类,一类是ScopeClassPath,一类是accessors classpath,compilationClassPath就是将2者加起来

  1. ScopeClassPath

ScopeClassPath是固有的一些classpath,不用编译plugin就自带,可以分为3种
用gradleLib表示$HOME/.gradle/wrapper/dists/gradle-version/hashcode/gradle-version/lib

  • gradleApi

    • .gradle/caches/version/generated-gradle-jars/gradle-api-version.jar
    • gradleLib/groovy相关jar包
    • gradleLib/kotlin标准库相关jar包
    • gradleLib/gradle-installation-beacon-version.jar 等等
  • gradleApiExtensions

    • gradle/cache/version/generated-gradle-jars/gradle-kotlin-dsl-extensions.jar
    • 给gradle api生成kotlin拓展方法的源码,例如files、repositories等,细节见ApiExtensionsJarGenerator
  • gradleKotlinDslJars

    • gradleLib/kotlin标准库相关jar包
    • gradleLib/gradle-kotlin-dsl-version.jar(这个jar包是gradle提供的基础的api的kotlin拓展方法,例如apply,dependencies,maven等。实际上这个jar包就是kotlin dsl的源码,上述的Interpreter、ResidualProgramCompiler等也在这个jar包里)
    • gradleLib/gradle-kotlin-dsl-tooling-models-version.jar
  • exportedClassPath

    • buildSrc jar包的classpath是在project对象prepare的过程中就给导入了的
    • includeBuild和plugins的classpath是在stage1代码在eval时,通过执行applyPlugin,调用DefaultPluginRequestApplicator.defineScriptHandlerClassScope导入的
  • accessors classpath

  • stage1

    • InterpreterHost.pluginAccessorsFor触发,最终会调用 GeneratePluginAccessors,从project的buildSrcClassLoaderScope找到pluginDescriptorsClassPath,和precompiled script流程中的GenerateExternalPluginSpecBuilders找plugin一样,之后对这些plugin生成accessor,以便在plugins block中可以被调用到,这里是为buildSrc引入的plugin生成PluginAccessors
      其次是在DefaultPluginRequestApplicator.applyPlugins中触发includeBuild的构建,生成对应的accessors
  • stage2

    • ProgramHost.accessorsClassPathFor触发,最终调用GenerateProjectAccessors,和precompiled script中的GeneratePrecompiledScriptPluginAccessorsGeneratePrecompiledScriptPluginAccessors类似,因为plugin在stage1阶段已经被apply了,所以这里可以从project对象获取到插件引入的task、extension等,为其生成accessors

生成的代码位于$HOME/.gradle/caches/*version*/kotlin-dsl/accessors

缓存

setting.gradle.ktsbuild.gradle.kts进行evaluate是Configuration阶段,被称为configuration cache

有2层缓存,内存缓存和文件缓存 文件缓存靠gradle本身task的执行流程机制保障,没有内存缓存时,gradle会创建一个类似task的执行流程,来加载kts脚本
内存缓存在StandardKotlinScriptEvaluator.classloadingCache里,如果是daemon运行方式(默认方式),这个进程是常驻的所以可以作为内存缓存,如果之前没有daemon进程例如首次启动,或者手动关闭了daemon进程例如-Dorg.gradle.daemon的话,就相当于没有内存缓存

打卡学习Gradle深度解析 - kts脚本加载流程相关推荐

  1. requirejs加载顺序_【requireJS源码学习03】细究requireJS的加载流程

    前言 这个星期折腾了一周,中间没有什么时间学习,周末又干了些其它事情,这个时候正好有时间,我们一起来继续学习requireJS吧 还是那句话,小钗觉得requireJS本身还是有点难度的,估计完全吸收 ...

  2. 从源码解析-结合Activity加载流程深入理解ActivityThrad的工作逻辑

    ActivityThread源码解析 前言 类简称 类简介 一 二 三 四 五 代理和桩的理解 ActivityThread ActivityThread.main AT.attach AMN.get ...

  3. 【框架源码】Spring源码解析之BeanDefinition加载流程解析

    观看本文之前,我们先思考一个问题,Spring是如何描述Bean对象的? Spring是根据BeanDefinition来创建Bean对象,BeanDefinition就是Spring中表示Bean定 ...

  4. 高性能JavaScript-JS脚本加载与执行对性能的影响

    在web产品优化准则中,很重要的一条是针对js脚本的加载和执行方式的优化.本篇文章简单描述一下其中的优化准则. 1. 脚本加载优化 1.1 脚本位置对性能的影响 优化页面加载性能的原则之一是将scri ...

  5. Android SIM卡识别加载流程

    文章目录 总述 代码路径 UICC框架 SIM卡识别加载流程 日志分析举例 总述 本文基于Android N(Android 7) 首先要知道SIM卡一般是挂载在CP侧(MODEM侧)的,由MODEM ...

  6. Google官方网络框架-Volley的使用解析Json以及加载网络图片方法

    Google官方网络框架-Volley的使用解析Json以及加载网络图片方法 Volley是什么?Google I/O 大会上,Google 推出 Volley的一个网络框架Volley适合什么场景? ...

  7. Launcher启动流程加载流程学习

     声明: 图片本来是有的 涉及到有些代码不能示人没有贴上,不过仅文字说也足够了,请广大老爷们自行下载源码参看流程分析阅读. 目录 一.认识Launcher: 1 1.功能 1 2.样式 2 3.And ...

  8. Dns-prefetch DNS 预解析优化页面加载速度

    Dns-prefetch DNS 预解析优化页面加载速度 浏览器访问一个链接时并不是直接将请求到网页对应的服务器上,而是先要做域名解析--将域名解析到网页对应的服务器 ip 地址,然后浏览器才能和服务 ...

  9. 从 RequireJs 源码剖析脚本加载原理

    引言 俗话说的好,不喜欢研究原理的程序员不是好的程序员,不喜欢读源码的程序员不是好的 jser.这两天看到了有关前端模块化的问题,才发现 JavaScript 社区为了前端工程化真是煞费苦心.今天研究 ...

最新文章

  1. linux 如何禁用账号和解除禁用账号
  2. ORA-01036: 非法的变量名/编号
  3. 【新概念第一册】Lesson_29 Come in,Amy.
  4. abap 给用户分配事物代码权限_【第五篇】SAP ABAP7.50 之用户接口
  5. ant-design 修改logo_北京师范大学珠海园区第一届山谷艺术节logo征集
  6. 微信小程序之底部弹框预约插件
  7. 数字基带调制解调matlab仿真,我的基于MATLAB仿真的数字调制与解调设计
  8. 【sdx62】uci软件包文件中新增脚本文件functions.sh但未打包到文件系统解决方案
  9. python在视频上方加字_使用moviepy给视频加字遇到的坑
  10. WIN10安装CUDA10 cuDNN
  11. Ubuntu 18.04 安装Unity3d
  12. 拯救脆弱的智慧城市:不但要“智商” 还得有“生气”
  13. 查看windows系统和office是否永久激活(转载)
  14. 搞不定Eclipse TPTP
  15. DataList绑定照片并分页
  16. Delphi导出word
  17. wxPython 2 - wxPython基础
  18. 【交互设计】七步打造手机网站
  19. 按丶自动打开计算机,联想电脑台式机启动自动进入Lenovo diagnostics界面
  20. 新攻击使黑客可以解密VoLTE加密以监视电话

热门文章

  1. 人工智能的发展能够对我们人类的生活造成巨大改变
  2. csh/tcsh脚本札记(持续更新:2021-8-24)
  3. Samba服务器的配置与使用
  4. android画渐变色三角形
  5. mysql中将列动态转换为行,mysql 行转列 MySQL数据库动态行转列
  6. 5年经验前端大佬在线收徒了.... 如何学习前端, 前端开发中的疑惑,进阶指南
  7. Hbase和Phoenix数据类型转换
  8. 如何重置wordpress用户密码
  9. 小程序体验版:登录不上/请求不到数据
  10. Google地图瓦片拼图算法解析