Android App 开发基础知识(1) —— Gradle 如何编译你的 Kotlin 代码
1. 前言
我们都知道基于 Gradle 构建框架做 Android 开发需要引入 Android Gradle Plugin(后文简写为 AGP)。但是该插件并不负责编译 Kotlin 源码相关内容。如果我们只引入 AGP,是无法写 Kotlin 代码的。
于是我们有 Kotlin Gradle Plugin 来帮忙编译 Kotlin 源码,这个插件和 AGP 是互相独立的,不要混淆
本文只是引导,想具体了解请看源码,没有想象中那么困难!
2. 如何在 Gradle 中引入 Kotlin 插件?
2.1 在 Gradle 构建环境中引入 Kotlin 插件的 classpath
app/build.gradle 中写:
buildscript {
dependencies {
classpath “`org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version`”
}
}
2.2 引入 Kotlin 插件
app/build.gradle 中写:
apply: plugin “kotlin-android”
此时你就可以在 src 中写 Kotlin 代码了,src/java 和 src/kotlin 都可以,但是更建议 java,因为 java 下面也可以写 Kotlin,反之则不行
3. Kotlin Gradle Plugin 是如何注册 Kotlin 编译任务的?
KGP 本质也是一个普通的 Gradle Plugin 插件而已,和 AGP 一样。因为只需要负责 Kotlin 相关的任务,所以会比 AGP 实现简单很多
在正式分析源码之前,我们先思考一个问题:如果让你实现一个 KGP,你会怎么实现?
如果是我来实现,我会这么考虑:
首先我们要继承这个接口(这是所有 Gradle 插件的入口):
/**
* <p>A <code>Plugin</code> represents an extension to Gradle. A plugin applies some configuration to a target object.
* Usually, this target object is a {@link org.gradle.api.Project}, but plugins can be applied to any type of
* objects.</p>
*
* @param <T> The type of object which this plugin can configure.
*/
public interface Plugin<T> {
/**
* Apply this plugin to the given target object.
*
* @param target The target object
*/
void apply(T target);
}
然后在 apply 里实现具体的逻辑,主要是实现 注册 Kotlin 编译相关的任务(compileKotlin? kaptGenerateStub?…)
听起来似乎非常简单,我们可以看下真正的 KGP 是否如我们思考的一样简单
让我们开始阅读 Kotlin 源码吧!
第一个难题是怎么找到入口
结合前面的 apply: plugin “kotlin-android”
了解 Gradle 原理的同学应该知道这意味着 Kotlin 源码下一定有一个名为 kotlin-android.properties 的文件记载了该插件的入口
我们找到这个文件,查看内容:
implementation-class=org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
可以知道 KGP 的入口是
org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
open class KotlinAndroidPluginWrapper @Inject constructor(
registry: ToolingModelBuilderRegistry
) : AbstractKotlinAndroidPluginWrapper(registry) {
override val pluginVariant: String = PLUGIN_VARIANT_NAME
override fun apply(project: Project) {
project.registerVariantImplementations()
super.apply(project)
}
}
Kotlin 源码代码风格就喜欢层层依赖,通过各种继承复用代码。因为我们只考虑 Kotlin JVM 平台,实际上 Kotlin 还能被编译到其他平台,所以 Kotlin 源码里有很多我们不需要关注的,比如 JS 平台之类的
但我们知道 KotlinAndroidPluginWrapper 一定会继承到 Gradle 的 Plugin
我把继承链画出来:
KotlinAndroidPluginWrapper
→
AbstractKotlinAndroidPluginWrapper
→
KotlinBasePluginWrapper
→
DefaultKotlinBasePlugin
→
KotlinBasePlugin
到最后一层其实就一个成员:
/**
* The base interface for all Kotlin Gradle plugins.
*/
interface KotlinBasePlugin : Plugin<Project> {
/**
* The current plugin version.
*/
val pluginVersion: String
}
Kotlin 就为了明确一下这个 plugin 有个 version 这个语义多做了一层继承…
DefaultKotlinBasePlugin
主要做了一些和注册编译任务本身无关的操作,注册了一些服务和后面可能用到的 extension,可以简单看下:
override fun apply(project: Project) {
/*
* 这一部分就是在检查运行环境,如果运行环境不符合预期直接抛出错误,
* 避免后续出现更多因为运行环境不正确,莫名其妙的问题
*/
project.checkCompilerEmbeddableInClasspath()
project.registerDefaultVariantImplementations()
if (!project.kotlinPropertiesProvider.buildDisableGradleVersionCheck) {
project.runGradleCompatibilityCheck()
project.runAgpCompatibilityCheckIfAgpIsApplied()
}
/*
* 注册乱七八糟的服务
*/
val buildUidService = BuildUidService.registerIfAbsent(project)
BuildFusService.registerIfAbsent(project, pluginVersion, buildUidService)
PropertiesBuildService.registerIfAbsent(project)
project.gradle.projectsEvaluated {
whenBuildEvaluated(project)
}
/*
* 比较深度的使用 Gradle 接口,给 project 添加 configurations
* configurations 简单理解就是 JAR
* 这里是需要 Kotlin Compiler 的相关 JAR 用于编译任务
* 注册给 Gradle,后面需要调用编译器可以直接调用
* 这一块主要和 Gradle 底层关系比较大,后续可以考虑出博客介绍 Gradle
*/
addKotlinCompilerConfiguration(project)
project.configurations.maybeCreateResolvable(PLUGIN_CLASSPATH_CONFIGURATION_NAME).apply {
isVisible = false
addGradlePluginMetadataAttributes(project)
}
val kotlinGradleBuildServices = KotlinGradleBuildServices.registerIfAbsent(project).get()
if (!project.isProjectIsolationEnabled) {
kotlinGradleBuildServices.detectKotlinPluginLoadedInMultipleProjects(project, pluginVersion)
}
BuildMetricsService.registerIfAbsent(project)
KotlinNativeBundleBuildService.registerIfAbsent(project)
}
KotlinBasePluginWrapper
中间层,因为这一层不是 Kotlin Android 专属的,同样有很多无关内容
override fun apply(project: Project) {
super.apply(project)
....
/*
* 重点关注这里的 plugin,就是 KotlinAndroidPlugin
*/
val plugin = getPlugin(project)
plugin.apply(project)
....
}
KotlinAndroidPlugin
这里才是主要的注册 Kotlin Android 相关任务的内容
override fun apply(project: Project) {
project.dynamicallyApplyWhenAndroidPluginIsApplied(
{
/*
* 这里先构建了一个 target
* 这段代码比较坑的点是 target 作为参数的方法基本都是在写 target
* 就是这个 target 是在慢慢变得完整的,一开始是个空的...
*/
val target = project.objects.KotlinAndroidTarget(project)
val kotlinAndroidExtension = project.kotlinExtension as KotlinAndroidProjectExtension
kotlinAndroidExtension.targetFuture.complete(target)
project.configureCompilerOptionsForTarget(
kotlinAndroidExtension.compilerOptions,
target.compilerOptions
)
kotlinAndroidExtension.compilerOptions.noJdk.value(true).disallowChanges()
@Suppress("DEPRECATION") val kotlinOptions = object : KotlinJvmOptions {
override val options: KotlinJvmCompilerOptions
get() = kotlinAndroidExtension.compilerOptions
}
/*
* Kotlin Android 必须和 AGP 一起使用,这里给 AGP 的拓展添加字段
* kotlinOptions
*/
val ext = project.extensions.getByName("android") as BaseExtension
ext.addExtension(KOTLIN_OPTIONS_DSL_NAME, kotlinOptions)
target
}
) { androidTarget ->
registry.register(KotlinModelBuilder(project.getKotlinPluginVersion(), androidTarget))
project.whenEvaluated { project.components.addAll(androidTarget.components) }
}
}
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
wasConfigured = true
val target = kotlinAndroidTargetProvider()
/*
* 这里就是最主要的填充 target 的地方
*/
androidTargetHandler().configureTarget(target)
additionalConfiguration(target)
}
}
重点看 configureTarget 的源码:
fun configureTarget(kotlinAndroidTarget: KotlinAndroidTarget) {
val project = kotlinAndroidTarget.project
val ext = project.extensions.getByName("android") as BaseExtension
/*
* 深入绑定 AGP 和 KGP 的 source
* 比如 KGP 为什么能识别 src/main/java 中的 kotlin 文件?
* PS: src/main/kotlin 里的 Java 文件不会被编译
*/
applyKotlinAndroidSourceSetLayout(kotlinAndroidTarget)
/*
* 再检查一波:这个 module(project) 一定要引入了 AGP
*/
androidPluginIds
.asSequence()
.mapNotNull { project.plugins.findPlugin(it) as? BasePlugin }
.firstOrNull()
?: throw InvalidPluginException(
"'kotlin-android' expects one of the Android Gradle " +
"plugins to be applied to the project:\n\t" +
androidPluginIds.joinToString("\n\t") { "* $it" }
)
/*
* 构建 Kotlin 编译任务
*/
project.forAllAndroidVariants { variant ->
// compilation 的工厂集合,里面包含了很多工厂
// 比如 KotlinCompilationOutputFactory 用于生成编译的输出位置
// 我认为这里明显是有些过度设计的!!!不要太纠结这里,Kotlin 源码很喜欢搞这样的
val compilationFactory = KotlinJvmAndroidCompilationFactory(kotlinAndroidTarget, variant)
val variantName = getVariantName(variant)
// Create the compilation and configure it first, then add to the compilations container. As this code is executed
// in afterEvaluate, a user's build script might have already attached item handlers to the compilations container, and those
// handlers might break when fired on a compilation that is not yet properly configured (e.g. KT-29964):
compilationFactory.create(variantName).let { compilation ->
setUpDependencyResolution(variant, compilation)
/*
* 这里创建 Kotlin 编译任务,就是 Gradle 构建流程中可以看见的 compileKotlin 之类的任务
*/
preprocessVariant(variant, compilation, project, kotlinTasksProvider)
@Suppress("UNCHECKED_CAST")
(kotlinAndroidTarget.compilations as NamedDomainObjectCollection<in KotlinJvmAndroidCompilation>).add(compilation)
}
}
/*
* Kotlin 编译任务
*/
project.whenEvaluated {
forAllAndroidVariants { variant ->
val compilation = kotlinAndroidTarget.compilations.getByName(getVariantName(variant))
postprocessVariant(variant, compilation, project, ext)
val subpluginEnvironment = SubpluginEnvironment.loadSubplugins(project)
subpluginEnvironment.addSubpluginOptions(project, compilation)
}
checkAndroidAnnotationProcessorDependencyUsage(project)
addKotlinDependenciesToAndroidSourceSets(project)
}
project.includeKotlinToolingMetadataInApk()
addAndroidUnitTestTasksAsDependenciesToAllTest(project)
}
我们在这里主要关注
preprocessVariant
和
preprocessVariant 即可
前者负责创建 Kotlin 任务,后者负责协调 Kotlin 任务和 Java 任务的先后顺序
PS:AGP 本身就支持 Java 任务,KGP 这里就是在适配 AGP 的 Java 任务
preprocessVariant
private fun preprocessVariant(
@Suppress("TYPEALIAS_EXPANSION_DEPRECATION") variantData: DeprecatedAndroidBaseVariant,
compilation: KotlinJvmAndroidCompilation,
project: Project,
tasksProvider: KotlinTasksProvider
) {
...
/*
* 这里就是 Kotlin 编译任务,主要要关注这个任务的 configure 包括什么动作
*/
val configAction = KotlinCompileConfig(KotlinCompilationInfo(compilation))
configAction.configureTask { task ->
task.useModuleDetection.value(true).disallowChanges()
// store kotlin classes in separate directory. They will serve as class-path to java compiler
task.destinationDirectory.set(project.layout.buildDirectory.dir("tmp/kotlin-classes/$variantDataName"))
task.description = "Compiles the $variantDataName kotlin."
}
...
}
configure 主要是两个类的 init
BaseKotlinCompileConfig
→
AbstractKotlinCompileConfig
Kotlin 中父类的 init 会先执行,我们先看 AbstractKotlinCompileConfig
init {
...
/*
* 这下面都是 task 的 configure 流程
* 其实看到这里后面一堆
* disallowChanges finalizeValueOnRead 之类的东西
* 大概就能看出来 Gradle 的生命周期设计的多复杂。。。
* 这里简单说一下 task 的类型是 KotlinCompile,这里就是填充这个类的字段而已
* 不需要详细看每一个 field 是在干啥
*/
configureTask { task ->
val propertiesProvider = project.kotlinPropertiesProvider
task.taskBuildCacheableOutputDirectory
.value(getKotlinBuildDir(task).map { it.dir("cacheable") })
.disallowChanges()
task.taskBuildLocalStateDirectory
.value(getKotlinBuildDir(task).map { it.dir("local-state") })
.disallowChanges()
task.localStateDirectories.from(task.taskBuildLocalStateDirectory).disallowChanges()
task.systemPropertiesService.value(compilerSystemPropertiesService).disallowChanges()
task.kotlinCompilerArgumentsLogLevel.value(propertiesProvider.kotlinCompilerArgumentsLogLevel).disallowChanges()
propertiesProvider.kotlinDaemonJvmArgs?.let { kotlinDaemonJvmArgs ->
task.kotlinDaemonJvmArguments.set(providers.provider {
kotlinDaemonJvmArgs.split("\\s+".toRegex())
})
}
task.compilerExecutionStrategy.convention(propertiesProvider.kotlinCompilerExecutionStrategy).finalizeValueOnRead()
task.useDaemonFallbackStrategy.convention(propertiesProvider.kotlinDaemonUseFallbackStrategy).finalizeValueOnRead()
task.suppressKotlinOptionsFreeArgsModificationWarning
.convention(propertiesProvider.kotlinOptionsSuppressFreeArgsModificationWarning)
.finalizeValueOnRead()
task.preciseCompilationResultsBackup
.convention(propertiesProvider.preciseCompilationResultsBackup)
.finalizeValueOnRead()
task.taskOutputsBackupExcludes.addAll(task.preciseCompilationResultsBackup.map {
if (it) listOf(task.destinationDirectory.get().asFile, task.taskBuildLocalStateDirectory.get().asFile) else emptyList()
})
task.keepIncrementalCompilationCachesInMemory
.convention(
task.preciseCompilationResultsBackup.zip(propertiesProvider.keepIncrementalCompilationCachesInMemory) { thisTaskPreciseCompilationResultsBackup, defaultKeepIncrementalCompilationCachesInMemory ->
thisTaskPreciseCompilationResultsBackup && defaultKeepIncrementalCompilationCachesInMemory
}
)
.finalizeValueOnRead()
task.taskOutputsBackupExcludes.addAll(task.keepIncrementalCompilationCachesInMemory.map {
if (it) listOf(task.taskBuildCacheableOutputDirectory.get().asFile) else emptyList()
})
task.enableUnsafeIncrementalCompilationForMultiplatform
.convention(propertiesProvider.enableUnsafeOptimizationsForMultiplatform)
.finalizeValueOnRead()
task.enableMonotonousIncrementalCompileSetExpansion
.convention(propertiesProvider.enableMonotonousIncrementalCompileSetExpansion)
.finalizeValueOnRead()
task.buildFinishedListenerService.value(buildFinishedListenerService).disallowChanges()
task.buildIdService.value(buildIdService).disallowChanges()
task.incremental = false
task.useModuleDetection.convention(false)
task.runViaBuildToolsApi.convention(propertiesProvider.runKotlinCompilerViaBuildToolsApi).finalizeValueOnRead()
task.explicitApiMode
.value(explicitApiMode)
.finalizeValueOnRead()
}
}
BaseKotlinCompileConfig
init {
configureTaskProvider { taskProvider ->
/*
* 这个 propertiesProvider 写的挺有意思的,一个负责 property set 和 get 服务的 service
*/
val useClasspathSnapshot = propertiesProvider.useClasspathSnapshot.get()
/*
* 可以关注一下这个 snapshot,这个是为了方便 Kotlin 增量编译引入的操作
* 后续会出博客单独讲述这个 snapshot
* 没有它也能增量编译,但是有了它效率更高
*/
val classpathConfiguration = if (useClasspathSnapshot) {
val jvmToolchain = taskProvider.flatMap { it.defaultKotlinJavaToolchain }
val runKotlinCompilerViaBuildToolsApi = propertiesProvider.runKotlinCompilerViaBuildToolsApi
registerTransformsOnce(project, jvmToolchain, runKotlinCompilerViaBuildToolsApi)
// Note: Creating configurations should be done during build configuration, not task configuration, to avoid issues with
// composite builds (e.g., https://issuetracker.google.com/183952598).
project.configurations.detachedResolvable(
project.dependencies.create(objectFactory.fileCollection().from(project.provider { taskProvider.get().libraries }))
)
} else null
if (useClasspathSnapshot) {
taskProvider.configure { it.incrementalModuleInfoProvider.disallowChanges() }
} else {
val incrementalModuleInfoProvider = IncrementalModuleInfoBuildService.registerIfAbsent(
project,
objectFactory.providerWithLazyConvention { GradleCompilerRunner.buildModulesInfo(project.gradle) },
)
taskProvider.configure { it.incrementalModuleInfoProvider.value(incrementalModuleInfoProvider).disallowChanges() }
}
taskProvider.configure { task ->
task.incremental = propertiesProvider.incrementalJvm ?: true
task.usePreciseJavaTracking = propertiesProvider.usePreciseJavaTracking ?: true
task.jvmTargetValidationMode.convention(propertiesProvider.jvmTargetValidationMode).finalizeValueOnRead()
task.classpathSnapshotProperties.useClasspathSnapshot.value(useClasspathSnapshot).disallowChanges()
if (useClasspathSnapshot) {
val classpathEntrySnapshotFiles = classpathConfiguration!!.incoming.artifactView {
it.attributes.setAttribute(ARTIFACT_TYPE_ATTRIBUTE, CLASSPATH_ENTRY_SNAPSHOT_ARTIFACT_TYPE)
}.files
task.classpathSnapshotProperties.classpathSnapshot.from(classpathEntrySnapshotFiles).disallowChanges()
task.classpathSnapshotProperties.classpathSnapshotDir.value(getClasspathSnapshotDir(task)).disallowChanges()
} else {
task.classpathSnapshotProperties.classpath.from(task.project.provider { task.libraries }).disallowChanges()
}
task.taskOutputsBackupExcludes.addAll(
task.classpathSnapshotProperties.classpathSnapshotDir.asFile.flatMap {
// it looks weird, but it's required to work around this issue: https://github.com/gradle/gradle/issues/17704
objectFactory.providerWithLazyConvention { listOf(it) }
}.orElse(emptyList())
)
}
}
}
到这里我们已经注册好了 KotlinCompile 这个任务,下一节具体来看这个任务的执行流程是什么样
4. KotlinCompile —— Kotlin 编译任务是如何调度 Kotlin 编译器编译 Kotlin 源码的
坐标 org.jetbrains.kotlin.gradle.tasks.KotlinCompile
这个任务的入口找 TaskAction 注解即可
继承链
KotlinCompile
→
AbstractKotlinCompile
→
AbstractKotlinCompileTool
→
DefaultTask
TaskAction 注解在 AbstractKotlinCompile
所以我们从这里看起
AbstractKotlinCompile.execute() ->
AbstractKotlinCompile.executeImpl() ->
KotlinCompile.callCompilerAsync() ->
GradleCompilerRunner.runJvmCompilerAsync() ->
GradleCompilerRunner.runCompilerAsync() ->
GradleCompilerRunnerWithWorkers.runCompilerAsync() ->
GradleCompilerRunnerWithWorkers.runCompilerAsync() 没有直接实行任务,而是给 Gradle 的
WorkerExecutor 执行,是异步的过程
执行的 Action 是 GradleKotlinCompilerWorkAction
GradleKotlinCompilerWork.run() ->
GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl()
这里根据 strategy 可能调用三种 compile,分别是:
/**
* Execute Kotlin compiler in its own daemon. Default strategy.
*
* Daemon may be shared across multiple compile tasks if it's considered compatible
*/
DAEMON("daemon"),
/**
* Execute Kotlin compiler inside the Gradle process
*
* Note: currently this strategy doesn't support incremental compilation
*/
IN_PROCESS("in-process"),
/**
* Execute Kotlin compiler in a new forked process for each compilation
*
* Note: currently this strategy doesn't support incremental compilation
*/
OUT_OF_PROCESS("out-of-process"),
;
kotlin.compiler.execution.strategy 可以用来设置编译的模式,默认是 daemon 模式
这里可以看见 daemon 是唯一可以增量编译的模式,所以对于超大型工程而言,尽量使用默认模式
GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl() ->
GradleKotlinCompilerWork.compileWithDaemon() ->
GradleKotlinCompilerWork.incrementalCompilationWithDaemon() ->
从这里开始就是 kotlin daemon
CompileServiceImpl.compile()
这里实际上是一个类似 RPC 的东西,看起来就像 client 直接调用函数一样,实际上是让 kotlin daemon(server) 执行的
最终会调用到
IncrementalJvmCompilerRunner.compile()
在这里面处理增量和实际编译的逻辑
如果想了解编译内部是怎么运行的,请查看:
org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
5. 其他内容
后面的博客方向:
- Gradle 深入分析
- Kotlin 编译器增量流程
- Java RMI 的实现原理