本文字数:22817字
预计阅读时间:58分钟
简介
KMM
, 即Kotlin Multiplatform Mobile
,是由Kotlin
发布的移动端跨平台框架。相比于其他跨平台框架,KMM
是原生UI+
逻辑共享的理念,共享重复逻辑性的工作来提升开发效率的同时,保持原生执行效率与UI
特性。所以KMM
并不会替代Android
和iOS
的原生开发, 而是提倡将共有的逻辑部分抽出,由KMM
封装成Android(Kotlin/JVM)
的aar
和iOS(Kotlin/Native)
的framework
,再提供给View
层进行调用,从而节约一部分的工作量。 我们先来比较几种当前流行的跨平台框架:
主要说两点:
由于
KMM
的本质就是原生App
,跨平台共享的内容都是在编译期进行的处理,所以在性能方面可以说是不受影响。其他的跨平台方案,其性能多多少少都会受到影响且会增加包体积。以
Flutter
为代表的自带渲染引擎实现UI
框架在开发效率上是更高的。而KMM
主要实现的是共享逻辑,UI
层的实现还是建议平台各自去处理,所以开发效率上来说,KMM
优于原生开发,但不如Flutter
。不过由于Android
的官方语言就是Kotlin
,对于Android
开发来说,KMM
的加持更像是一种赠送能力,几乎可以无成本的进行KMM
开发。
另外,去年十月初Android 官方宣布 Jetpack
开始要支持KMM
了,意味着KMM
已经得到了官方的支持。目前Collections
和 DataStore
已经可以通过依赖 -dev01
版本在多平台上使用,加上KMM
已经到了Beta
的阶段,到了我们可以进行大胆尝试的时候了。因此本文将从准备工作,结构介绍,Demo
示例和编译过程详解来对KMM
框架进行说明。
准备工作
安装必要的工具和插件
1.安装或更新
AndroidStudio和Xcode
到最新版2.
JDK
3.
Kotlin Multiplatform Mobile plugin:
从AndroidStudio
下载KMM
插件,如图所示:
4.
Kotlin plugin:
更新Kolin Plugin
到最新版本,如图所示:
检查环境配置
1.打开
Terminal
使用HomeBrew
安装以下工具:
brew install kdoctor
2.执行
kdoctor:
kdoctor
如果你的环境配置有问题,那么将会输出相应的信息,并以X
作为标记,大家可根据提示进行修改。Tips:
以上步骤可能需要科学上网。
KMM结构说明
在尝试KMM
之前,我们先来了解一下KMM
的基本结构。我们建个默认的KMM
工程,看一下它的结构:默认会生成androidApp
,shared
和iosApp
这三个子工程。其中androidApp
和iosApp
为Android和iOS这两个平台的工程模块,shared
为共享逻辑模块,供androidApp
和iosApp
调用。我们打开根目录的settings.gradle.kts
:
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "My_Application"
include(":androidApp")
include(":shared")
会发现主项目只include
了androidApp
和shared
这两个子项目,因为这两个项目是Gradle
项目,那么iOS
的项目如何引用呢?我们摘抄一段官网的原文:
The iOS application is produced from an Xcode project. It's stored in a separate directory within the root project. Xcode uses its own build system; thus, the iOS application project isn't connected with other parts of the Multiplatform Mobile project via Gradle. Instead, it uses the shared module as an external artifact – framework.
意思是说iOS
作为Xcode
项目,储存在根项目的另一个文件夹。Xcode
有自己的编译系统,因此iOS
项目并不依靠Gradle
去和共享工程建立联系,而是依靠将共享工程打包成framework
供iOS
项目使用。我们可以看一下shared
模块编译后的产物,如下图所示:
可以很清晰的看到生成的framework
和aar
文件。我们再来看看shared
模块都包含了什么:
其中
commonMain
为公共模块,该模块的代码与平台无关,是通过expected
关键字对一些api
的声明(声明的实现在platform module
中)。androidMain
和iosMain
分别为Android
和iOS
这两个平台,通过actual
关键字在平台模块进行具体的实现。
我们继续看看shared
模块的gradle
文件都做了什么:
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
}
这是将这个KMM
模块编译成Android aar
和iOS framework
的声明。使用了Gradle
编译系统和KMM
插件来进行实现。其中:
kotlin("multiplatform")
意味着引入KMM
插件。
id("com.android.library")
意味着生成一个Android aar
,其配置用android {}
进行了包裹:
android {
namespace = "com.example.myapplication"
compileSdk = 32
defaultConfig {
minSdk = 28
targetSdk = 32
}
}
iOS framework
是使用Kotlin/Native
进行编译的,相应的配置是用iosXXX{}
进行了包裹:
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
定义了输出格式为framework
,输出名称为shared
。我们接着看kotlin
还包含了什么:
kotlin {
//...
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}
}
val androidMain by getting{
dependencies {
implementation("io.ktor:ktor-client-android:$ktorVersion")
}
}
//...
val iosMain by creating {
//...
dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
//...
}
}
在上一节我们提到的commonMain
,androidMain
和iosMain
就是sourceSets
,其中commonMain
是共享的逻辑,而androidMain
和iosMain
是对Android
和iOS
共享逻辑的相应实现。每一个sourceSet
都可以有自己单独的dependencies
,如上面的代码。支持分别引入implementation
来实现各自的逻辑。另外Kotlin
标准库会被自动加到相应的sourceSet
中,无需重复引入。比方说上面的kotlinx-coroutines-core
。之后,Android
和iOS
分别使用各自常规方式引入/调用aar
或framework
即可。经过以上的说明,我们大概了解了KMM
,项目的架构,抄一张官网的图进行总结:
Demo示例
环境配置好了,我们就可以写一个简单的demo
了。打开Android Studio
,File->New->New Project
,选择Kotlin Multiplatform App
点击Next
:
继续点击Next
:
配置iOS
项目时,将iOS framework distribution
中选择Regular framework
,然后Finish
。至于为什么选择Regular framework
,官方文档的描述如下:
We recommend using the regular framework for your first project, as this option doesn't require third-party tools and has less installation issues. For more complex projects, you might need the CocoaPods dependency manager that helps handle library dependencies. To learn more about CocoaPods and how to set up an environment for them, see CocoaPods overview and setup.
大概意思是说Regular framework
不需要三方工具,且有较少的安装事项。对于更复杂的工程来说,可能需要使用CocoaPods dependency manage
去管理libs
。对于我们的Demo
工程来说,先使用Regular framework
来进行说明。在上个章节我们对于KMM
工程的结构进行了说明,我们进入shared
模块,看commonMain
文件夹下Greeting
的实现:
class Greeting {
private val platform: Platform = getPlatform()
fun greeting(): String {
return "Hello, ${platform.name}!"
}
}
greeting()
方法调用platform.name
,Platform
的实现如下:
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
Platform
是个接口,使用expect
关键字来声明getPlatform()
,再由Android
和iOS
通过使用actual
关键字分别实现:
Android
:
class AndroidPlatform : Platform {
override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
iOS
:
class IOSPlatform: Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()
最后我们看一下是如何调用的:
Android
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Greeting(Greeting().greeting())
}
}
}
}
}
@Composable
fun Greeting(text: String) {
Text(text = text)
}
iOS
:
struct ContentView: View {
let greet = Greeting().greeting()
var body: some View {
Text(greet)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
我们运行一下。
Android
的话,在Android Studio
上的configurations list
中选择androidApp
,直接连接真机或开启模拟器,点击Run->Run 'androidApp
'即可。iOS
的话,如果是第一次运行,需要打开Xcode
,同意一些协议,然后回到Android Studio
,configurations list
中选择iosApp
,直接运行即可。
效果如下:
编译过程详解
接下来我们来看一下shared
模块是如何工作的。先关注一下shared
模块的gradle
文件:
plugins {
kotlin("multiplatform")
id("com.android.library")
}
加载了multiplatform
插件,并把此module
作为lib
输出。
kotlin {
android()
iosX64()
iosArm64()
iosSimulatorArm64()
}
重点来了,这个是编译双端lib
的关键。我们先看看android()
是怎么编译Android
的aar
的:
Android编译过程
fun android() = android("android") { }
fun android(
name: String = "android",
configure: KotlinAndroidTarget.() -> Unit = { }
): KotlinAndroidTarget =
configureOrCreate(
name,
presets.getByName("android") as KotlinAndroidTargetPreset,
configure
)
我们看到android()
是创建了个KotlinAndroidTarget
对象。我们找到插件multiplatform
的入口类:AbstractKotlinMultiplatformPluginWrapper
:
abstract class AbstractKotlinMultiplatformPluginWrapper : KotlinBasePluginWrapper() {
override fun getPlugin(project: Project): Plugin<Project> =
KotlinMultiplatformPlugin()
override val projectExtensionClass: KClass<out KotlinMultiplatformExtension>
get() = KotlinMultiplatformExtension::class
override fun whenBuildEvaluated(project: Project) {
project.runMissingAndroidTargetProjectConfigurationHealthCheck()
project.runMissingKotlinTargetsProjectConfigurationHealthCheck()
project.runDisabledCInteropCommonizationOnHmppProjectConfigurationHealthCheck()
}
}
返回一个KotlinMultiplatformPlugin
对象。KotlinMultiplatformPlugin
继承Plugin
,我们直接看apply()
方法的实现:
override fun apply(project: Project) {
//...
setupDefaultPresets(project)
customizeKotlinDependencies(project)
configureSourceSets(project)
//...
截取重要的部分,看setupDefaultPresets(project)
的实现:
fun setupDefaultPresets(project: Project) {
with(project.multiplatformExtension.presets) {
add(KotlinJvmTargetPreset(project))
add(KotlinJsTargetPreset(project).apply { irPreset = null })
add(KotlinJsIrTargetPreset(project, isWasm = false).apply { mixedMode = false })
add(
KotlinJsTargetPreset(project).apply {
irPreset = KotlinJsIrTargetPreset(project, isWasm = false)
.apply { mixedMode = true }
}
)
add(KotlinJsIrTargetPreset(project, isWasm = true).apply { mixedMode = false })
add(KotlinAndroidTargetPreset(project))
add(KotlinJvmWithJavaTargetPreset(project))
// Note: modifying these sets should also be reflected in the DSL code generator, see 'presetEntries.kt'
val nativeTargetsWithHostTests = setOf(LINUX_X64, MACOS_X64, MACOS_ARM64, MINGW_X64)
val nativeTargetsWithSimulatorTests =
setOf(IOS_X64, IOS_SIMULATOR_ARM64, WATCHOS_X86, WATCHOS_X64, WATCHOS_SIMULATOR_ARM64, TVOS_X64, TVOS_SIMULATOR_ARM64)
HostManager().targets
.forEach { (_, konanTarget) ->
val targetToAdd = when (konanTarget) {
in nativeTargetsWithHostTests ->
KotlinNativeTargetWithHostTestsPreset(konanTarget.presetName, project, konanTarget)
in nativeTargetsWithSimulatorTests ->
KotlinNativeTargetWithSimulatorTestsPreset(konanTarget.presetName, project, konanTarget)
else -> KotlinNativeTargetPreset(konanTarget.presetName, project, konanTarget)
}
add(targetToAdd)
}
}
}
这个方法创建并添加了各个平台的TargetPreset
,看上去是用来配置各个平台的编译详情的。我们先看看Android
平台KotlinAndroidTargetPreset
的是如何创建的:
override fun createTarget(name: String): KotlinAndroidTarget {
val result = KotlinAndroidTarget(name, project).apply {
disambiguationClassifier = name
preset = this@KotlinAndroidTargetPreset
targetUnderConstruction = this
}
project.dynamicallyApplyWhenAndroidPluginIsApplied({ result })
if (project.hasKpmModel) {
mapTargetCompilationsToKpmVariants(result, PublicationRegistrationMode.AFTER_EVALUATE)
}
targetUnderConstruction = null
return result
}
创建了一个KotlinAndroidTarget
对象,并执行dynamicallyApplyWhenAndroidPluginIsApplied()
方法:
internal fun Project.dynamicallyApplyWhenAndroidPluginIsApplied(
kotlinAndroidTargetProvider: () -> KotlinAndroidTarget,
additionalConfiguration: (KotlinAndroidTarget) -> Unit = {}
) {
var wasConfigured = false
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
wasConfigured = true
val target = kotlinAndroidTargetProvider()
androidTargetHandler().configureTarget(target)
additionalConfiguration(target)
}
}
//...
}
看重点:
androidTargetHandler().configureTarget(target)
fun configureTarget(kotlinAndroidTarget: KotlinAndroidTarget) {
//...
project.forEachVariant { variant ->
val variantName = getVariantName(variant)
//...
kotlinAndroidTarget.compilationFactory.create(variantName).let { compilation ->
compilation.androidVariant = variant
setUpDependencyResolution(variant, compilation)
preprocessVariant(variant, compilation, project, kotlinOptions, kotlinConfigurationTools.kotlinTasksProvider)
@Suppress("UNCHECKED_CAST")
(kotlinAndroidTarget.compilations as NamedDomainObjectCollection<in KotlinJvmAndroidCompilation>).add(compilation)
}
}
project.whenEvaluated {
forEachVariant { variant ->
val compilation = kotlinAndroidTarget.compilations.getByName(getVariantName(variant))
postprocessVariant(variant, compilation, project, ext, plugin)
val subpluginEnvironment = SubpluginEnvironment.loadSubplugins(project)
subpluginEnvironment.addSubpluginOptions(project, compilation)
}
//...
}
//...
}
先看preprocessVariant()
的实现:
private fun preprocessVariant(
variantData: BaseVariant,
compilation: KotlinJvmAndroidCompilation,
project: Project,
rootKotlinOptions: KotlinJvmOptionsImpl,
tasksProvider: KotlinTasksProvider
) {
//...
tasksProvider.registerKotlinJVMTask(project, compilation.compileKotlinTaskName, compilation.kotlinOptions, configAction)
//...
}
做了一些初始配置,并为tasksProvider
注册了KotlinJVMTask
。看postprocessVariant()
的实现:
private fun postprocessVariant(
variantData: BaseVariant,
compilation: KotlinJvmAndroidCompilation,
project: Project,
androidExt: BaseExtension,
androidPlugin: BasePlugin
) {
//...
val javaTask = variantData.getJavaTaskProvider()
val kotlinTask = compilation.compileKotlinTaskProvider
//...
wireKotlinTasks(project, compilation, androidPlugin, androidExt, variantData, javaTask, kotlinTask)
}
创建了javaTask
和kotlinTask
,然后看wireKotlinTasks()
的实现:
override fun wireKotlinTasks(
project: Project,
compilation: KotlinJvmAndroidCompilation,
androidPlugin: BasePlugin,
androidExt: BaseExtension,
variantData: BaseVariant,
javaTask: TaskProvider<out AbstractCompile>,
kotlinTask: TaskProvider<out KotlinCompile>
) {
val preJavaKotlinOutput = project.files(project.provider {
mutableListOf<File>().apply {
add(kotlinTask.get().destinationDirectory.get().asFile)
if (Kapt3GradleSubplugin.isEnabled(project)) {
// Add Kapt3 output as well, since there's no SyncOutputTask with the new API
val kaptClasssesDir = Kapt3GradleSubplugin.getKaptGeneratedClassesDir(project, getVariantName(variantData))
add(kaptClasssesDir)
}
}
}).builtBy(kotlinTask)
val preJavaClasspathKey = variantData.registerPreJavacGeneratedBytecode(preJavaKotlinOutput)
kotlinTask.configure { kotlinTaskInstance ->
kotlinTaskInstance.libraries
.from(variantData.getCompileClasspath(preJavaClasspathKey))
.from(Callable { AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt) })
kotlinTaskInstance.javaOutputDir.set(javaTask.flatMap { it.destinationDirectory })
}
//...
}
从方法命名我们可得知,它的目的是执行KotlinTasks
。我们看见kotlinTask
是个TaskProvider
对象,实际的操作在KotlinCompile
里。KotlinCompile
是个Task
,其execute
实现在父类AbstractKotlinCompile
中:
@TaskAction
fun execute(inputChanges: InputChanges) {
val buildMetrics = metrics.get()
buildMetrics.measure(BuildTime.GRADLE_TASK_ACTION) {
//...
executeImpl(inputChanges, outputsBackup)
}
buildMetricsReporterService.orNull?.also { it.addTask(path, this.javaClass, buildMetrics) }
}
private fun executeImpl(
inputChanges: InputChanges,
taskOutputsBackup: TaskOutputsBackup?
) {
//...
callCompilerAsync(
args,
allKotlinSources,
inputChanges,
taskOutputsBackup
)
}
主要看执行在executeImpl()
中的callCompilerAsync()
方法:
override fun callCompilerAsync(
args: K2JVMCompilerArguments,
kotlinSources: Set<File>,
inputChanges: InputChanges,
taskOutputsBackup: TaskOutputsBackup?
) {
validateKotlinAndJavaHasSameTargetCompatibility(args, kotlinSources)
//...
val compilerRunner = compilerRunner.get()
//...
compilerRunner.runJvmCompilerAsync(
(kotlinSources + scriptSources).toList(),
commonSourceSet.toList(),
javaSources.files, // we need here only directories where Java sources are located
javaPackagePrefix,
args,
environment,
defaultKotlinJavaToolchain.get().providedJvm.get().javaHome,
taskOutputsBackup
)
}
先获取一个compilerRunner
,它的实现如下:
@get:Internal
override val compilerRunner: Provider<GradleCompilerRunner> = objectFactory.propertyWithConvention(
// From Gradle 6.6 better to replace flatMap with provider.zip()
defaultKotlinJavaToolchain.flatMap { toolchain ->
objectFactory.property(gradleCompileTaskProvider.map {
GradleCompilerRunnerWithWorkers(
it,
toolchain.currentJvmJdkToolsJar.orNull,
normalizedKotlinDaemonJvmArguments.orNull,
metrics.get(),
compilerExecutionStrategy.get(),
workerExecutor
)
})
}
)
创建了一个GradleCompilerRunnerWithWorkers
对象,再执行compilerRunner.runJvmCompilerAsync()
方法。继续追踪若干个调用,执行了如下代码:
protected open fun runCompilerAsync(
workArgs: GradleKotlinCompilerWorkArguments,
taskOutputsBackup: TaskOutputsBackup?
): WorkQueue? {
try {
val kotlinCompilerRunnable = GradleKotlinCompilerWork(workArgs)
kotlinCompilerRunnable.run()
} catch (e: GradleException) {
//...
}
return null
}
其中GradleKotlinCompilerWork
是个Runnable
,执行了它的run()
方法:
override fun run() {
try {
val messageCollector = GradlePrintingMessageCollector(log, allWarningsAsErrors)
val (exitCode, executionStrategy) = compileWithDaemonOrFallbackImpl(messageCollector)
//...
} finally {
//...
}
}
继续看的compileWithDaemonOrFallbackImpl()
实现:
private fun compileWithDaemonOrFallbackImpl(messageCollector: MessageCollector): Pair<ExitCode, KotlinCompilerExecutionStrategy> {
//...
if (compilerExecutionStrategy == KotlinCompilerExecutionStrategy.DAEMON) {
val daemonExitCode = compileWithDaemon(messageCollector)
//...
}
val isGradleDaemonUsed = System.getProperty("org.gradle.daemon")?.let(String::toBoolean)
return if (compilerExecutionStrategy == KotlinCompilerExecutionStrategy.IN_PROCESS || isGradleDaemonUsed == false) {
compileInProcess(messageCollector) to KotlinCompilerExecutionStrategy.IN_PROCESS
} else {
compileOutOfProcess() to KotlinCompilerExecutionStrategy.OUT_OF_PROCESS
}
}
可以看出,kotlin
编译有三种策略,分别是
守护进程编译:
Kotlin
编译的默认模式,只有这种模式才支持增量编译,可以在多个Gradle daemon
进程间共享进程内编译:
Gradle daemon
进程内编译进程外编译:每次编译都是在不同的进程 我们来看
Kotlin
编译的默认模式的实现,其compileWithDaemon()
方法最终执行了编译逻辑:
private fun compileWithDaemon(messageCollector: MessageCollector): ExitCode? {
//...
val bufferingMessageCollector = GradleBufferingMessageCollector()
val exitCode = try {
val res = if (isIncremental) {
incrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
} else {
nonIncrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
}
//...
} catch (e: Throwable) {
//...
}
//...
return exitCode
}
到这里会执行实现类org.jetbrains.kotlin.daemon.CompileServiceImpl
的 compile
方法,这样就终于调到了Kotlin
编译器内部。经过代码追踪,会先执行compileImpl()
方法,然后执行到doCompile()
方法:
val compiler = when (targetPlatform) {
CompileService.TargetPlatform.JVM -> K2JVMCompiler()
CompileService.TargetPlatform.JS -> K2JSCompiler()
CompileService.TargetPlatform.METADATA -> K2MetadataCompiler()
} as CLICompiler<CommonCompilerArguments>
doCompile(sessionId, daemonReporter, tracer = null) { eventManger, profiler ->
val services = createServices(servicesFacade, eventManger, profiler)
compiler.exec(messageCollector, services, k2PlatformArgs)
}
我们继续看 compiler.exec()
做了什么,跟进到K2JVMCompiler
的doExecute()
方法:在实现了一系列的配置之后,我们找到了关键代码:
KotlinToJVMBytecodeCompiler.compileModules(environment, buildFile, chunk)
继续跟踪compileModules()
的关键代码:
val codegenInputs = ArrayList<CodegenFactory.CodegenInput>(chunk.size)
for (module in chunk) {
//...
codegenInputs += runLowerings(
environment, moduleConfiguration, result, ktFiles, module, codegenFactory, backendInput, diagnosticsReporter
)
}
val outputs = ArrayList<GenerationState>(chunk.size)
for (input in codegenInputs) {
outputs += runCodegen(input, input.state, codegenFactory, result.bindingContext, diagnosticsReporter, environment.configuration)
}
return writeOutputs(environment.project, projectConfiguration, chunk, outputs, mainClassFqName)
runLowerings()
和runCodeGen()
看起来是我们想要的关键,其会执行到继续追踪关键代码:
return codegenFactory.invokeLowerings(state, backendInput)
.also { performanceManager?.notifyIRLoweringFinished()
codegenFactory.invokeCodegen(codegenInput)
追踪到CodegenFactory
的相应实现。又经过若干跳转后,执行到MemberCodegen.genSimpleMember()
。我们看看相应的实现:
public void genSimpleMember(@NotNull KtDeclaration declaration) {
if (declaration instanceof KtNamedFunction) {
try {
functionCodegen.gen((KtNamedFunction) declaration);
}
catch (ProcessCanceledException | CompilationException e) {
throw e;
}
catch (Exception e) {
throw new CompilationException("Failed to generate function " + declaration.getName(), e, declaration);
}
}
else if (declaration instanceof KtProperty) {
try {
propertyCodegen.gen((KtProperty) declaration);
}
catch (ProcessCanceledException | CompilationException e) {
throw e;
}
catch (Exception e) {
throw new CompilationException("Failed to generate property " + declaration.getName(), e, declaration);
}
}
//...
}
根据declaration
类型来决定用哪个Codegen
去编译,我们看方法的生成: functionCodegen.gen()
的关键实现:
generateMethod(JvmDeclarationOriginKt.OtherOrigin(function, functionDescriptor), functionDescriptor, strategy);
public void generateMethod(
@NotNull JvmDeclarationOrigin origin,
@NotNull FunctionDescriptor functionDescriptor,
@NotNull MethodContext methodContext,
@NotNull FunctionGenerationStrategy strategy
) {
//...
Method asmMethod = jvmSignature.getAsmMethod();
//...
MethodVisitor mv =
strategy.wrapMethodVisitor(
newMethod(
origin,
flags,
asmMethod.getName(),
asmMethod.getDescriptor(),
strategy.skipGenericSignature() ? null : jvmSignature.getGenericsSignature(),
getThrownExceptions(functionDescriptor, typeMapper)
),
flags, asmMethod.getName(),
asmMethod.getDescriptor()
);
//..
generateMethodBody(
origin, functionDescriptor, methodContext, strategy, mv, jvmSignature, staticInCompanionObject
);
//...
}
这段代码就是具体的编译过程了,从命名来看,大概就是使用ASM
框架去生成Java
字节码,其中mv
是个MethodVisitor
对象,在org.jetbrains.org.objectweb.asm
包内。Android
的编译过程我们就追踪到这里。
总结一下,
Android aar
的编译过程是由KotlinMultiplatformPlugin
发起kotlinTask
,再由kotlinTask
开启KotlinCompile
编译任务,并交给GradleCompilerRunnerWithWorkers
执行。在执行过程中调到了Kotlin
编译器内部org.jetbrains.kotlin.daemon.CompileServiceImpl的compile()
方法,并交由CodegenFactory
实现,最终使用ASM
框架去生成Java
字节码。
Android
的编译过程就看到这里,我们再来看看iOS
的,由于本人是Android
开发,对于iOS
的编译过程只做个大概的说明。
iOS编译过程
我们重新回到KotlinMultiplatformPlugin
的setupDefaultPresets()
找到iOS
相应的配置方法:
val nativeTargetsWithSimulatorTests =
setOf(IOS_X64, IOS_SIMULATOR_ARM64, WATCHOS_X86, WATCHOS_X64, WATCHOS_SIMULATOR_ARM64, TVOS_X64, TVOS_SIMULATOR_ARM64)
HostManager().targets
.forEach { (_, konanTarget) ->
val targetToAdd = when (konanTarget) {
in nativeTargetsWithHostTests ->
KotlinNativeTargetWithHostTestsPreset(konanTarget.presetName, project, konanTarget)
in nativeTargetsWithSimulatorTests ->
KotlinNativeTargetWithSimulatorTestsPreset(konanTarget.presetName, project, konanTarget)
else -> KotlinNativeTargetPreset(konanTarget.presetName, project, konanTarget)
}
add(targetToAdd)
}
我们选取一个Preset
看它的实现,比如KotlinNativeTargetPreset
,我们先看它父类AbstractKotlinNativeTargetPreset
的初始化:
override fun createTarget(name: String): T {
setupNativeCompiler()
val result = instantiateTarget(name).apply {
targetName = name
disambiguationClassifier = name
preset = this@AbstractKotlinNativeTargetPreset
val compilationFactory =
if (project.hasKpmModel) {
val kpmVariantClass = kpmNativeVariantClass(konanTarget) ?: error("Can't find the KPM variant class for $konanTarget")
KotlinMappedNativeCompilationFactory(this, kpmVariantClass)
} else {
KotlinNativeCompilationFactory(this)
}
compilations = project.container(compilationFactory.itemClass, compilationFactory)
}
createTargetConfigurator().configureTarget(result)
//...
return result
}
其中setupNativeCompiler()
方法的主要作用是创建一个NativeCompilerDownloader
对象,下载一些必要的工具,然后创建一个KotlinCompilationFactory
对象,编译执行的重点在:
createTargetConfigurator().configureTarget(result)
我们看一下createTargetConfigurator()
创建了什么:
override fun createTargetConfigurator(): AbstractKotlinTargetConfigurator<KotlinNativeTarget> {
val configurator = KotlinNativeTargetConfigurator<KotlinNativeTarget>()
return if (project.hasKpmModel)
KpmNativeTargetConfigurator(configurator)
else configurator
}
创建了一个KotlinNativeTargetConfigurator
对象,接下来我们看看configureTarget()
的实现:
fun configureTarget(
target: KotlinTargetType
) {
configureCompilationDefaults(target)
configureCompilations(target)
defineConfigurationsForTarget(target)
configureArchivesAndComponent(target)
configureSourceSet(target)
configureBuild(target)
configurePlatformSpecificModel(target)
}
做了很多配置,我们看看针对iOS
单独做了什么,继续看configurePlatformSpecificModel()
的实现:
override fun configurePlatformSpecificModel(target: T) {
configureBinaries(target)
configureFrameworkExport(target)
configureCInterops(target)
if (target.konanTarget.family.isAppleFamily) {
registerEmbedAndSignAppleFrameworkTasks(target)
}
if (PropertiesProvider(target.project).ignoreIncorrectNativeDependencies != true) {
warnAboutIncorrectDependencies(target)
}
}
我们看一下configureBinaries(target)
的主要实现:
target.binaries.all {
project.createLinkTask(it)
}
创建了一个linkTask
:
private fun Project.createLinkTask(binary: NativeBinary) {
//...
if (binary is Framework) {
createFrameworkArtifact(binary, result)
}
}
继续追踪createFrameworkArtifact()
:
private fun Project.createFrameworkArtifact(
binary: Framework,
linkTask: TaskProvider<KotlinNativeLink>
) {
configurations.create(lowerCamelCaseName(binary.name, binary.target.name)) {
it.isCanBeConsumed = true
it.isCanBeResolved = false
it.configureConfiguration(linkTask)
}
if (FatFrameworkTask.isSupportedTarget(binary.target)) {
configureFatFramework()
}
}
先执行了configureConfiguration()
,后执行了configureFatFramework()
,其中linkTask
为FatFrameworkTask
,编译任务最重都是由FatFrameworkTask
来执行的,其编译过程在如下方法里:
@TaskAction
protected fun createFatFramework() {
val outFramework = fatFrameworkLayout
outFramework.mkdirs()
mergeBinaries(outFramework.binary)
mergeHeaders(outFramework.header)
createModuleFile(outFramework.moduleFile, fatFrameworkName)
mergePlists(outFramework.infoPlist, fatFrameworkName)
mergeDSYM()
}
最终生成了可被iOS
引用的framework
。如果我们解压framework
文件,会发现这些merge
方法与其framework
的产物是一一对应的。
总结
好了,对于KMM
的介绍就到这里。KMM
的特性意味着我们可以大胆的使用它,不用担心性能问题,上架风险等。虽然它还不够成熟,支持性还不够高,但考虑到它较低的学习成本(特别是对于Andorid
开发来说),我们完全可以局部的使用它。对于前端开发同学来说,跨平台是个总也绕不开的话题,学习不同的框架对于我们对App
整体架构的思考也起到积极的作用。
参考文献
https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html
https://www.jianshu.com/p/bd0cf9b2193c
https://blog.csdn.net/rain_butterfly/article/details/87941589