初探 Kotlin Multiplatform Mobile 跨平台原理
一、背景
本文会尝试通过 KMM 编译产物理解一套 kt 代码是如何在多个平台复用的。
KMM 发流程简介
我以开发一个 KMM 日志库为例,简单介绍开发流程是什么:
- 在 CommonMain 定义接口,用
expect
关键字修饰,表示此接口在不同平台的实现不一样。 - 在具体平台实现接口,并用
actual
关键字修饰
// ----- commonMain -----
expect fun log(tag: String, msg: String)
// ----- androidMain -----
actual fun log(tag: String, msg: String) {
Log.i(tag, msg)
}
// ----- iosMain -----
actual fun log(tag: String, msg: String) {
NSLog("$tag:: %s", msg)
}
- 编译、打包、发布
- 依赖具体平台仓库
- 如果宿主为 Android App,则依赖对应的
kmm-infra-android
- 如果宿主为 iOS App,需要现将
kmm-infra-iosarm64
打包成 Framework,然后 iOS 依赖 Framework - 如果宿主为 KMM 库,则依赖
kmm-infra
二、Common 和具体平台的联系
了解 KMM 基本的开发流程和发布产物后,我们需要继续深入了解发布产物的结构,再来理解 Common 层代码和具体平台代码是如何建立联系的。
Common 层编译产物
├── kmm-infra
├── 1.0.0-SNAPSHOT
├── kmm-infra-1.0.0-SNAPSHOT-kotlin-tooling-metadata.json
├── kmm-infra-1.0.0-SNAPSHOT-sources.jar
├── kmm-infra-1.0.0-SNAPSHOT.jar
├── kmm-infra-1.0.0-SNAPSHOT.module
├── kmm-infra-1.0.0-SNAPSHOT.pom
└── maven-metadata-local.xml
kotlin-tooling-metadata.json
,存放了编译工具的相关信息,比如 gradle 版本、KMM 插件版本以及具体平台编译工具的信息,比如 jvm 平台会有 jdk 版本,native 平台会有 konan 版本信息source.jar
,Kotlin 源码.jar
,存放.knm
(knm是什么,后文会具体介绍) ,其中描述了expect
的接口.module
,见下文
.module 是什么?
用 json 描述编译产物文件结构的清单文件,以及关联 common 和具体平台产物的信息。里面描述的字段较多,我只放一些关键信息,剩余内容感兴趣的读者可以自己研究
{
"variants": [
{
"name": "",
"attributes": {
"org.gradle.category": "",
"org.gradle.usage": "",
"org.jetbrains.kotlin.platform.type": ""
}
"available-at": {
"url": "",
},
"dependencies": [
{
"group": "org.jetbrains.kotlin",
"module": "kotlin-stdlib-common",
"version": {
"requires": "1.8.0"
}
}
]
}
]
}
name
,当前产物的名称,比如 common 层为metadataApiElements
,具体平台为{target}{Api/Metadata}Elements-published
available-at
,具体平台特有的字段,其中url
指的是具体平台 .module 的文件路径,作为关联 common 和具体平台的桥梁dependencies
,描述有哪些依赖
具体平台的 .module
为方便大家更好的理解,这里还是贴出一份完整的 iOS 平台的 .module 文件
{
"formatVersion": "1.1",
"component": {
"url": "../../kmm-infra/1.0.0-SNAPSHOT/kmm-infra-1.0.0-SNAPSHOT.module",
"group": "com.gpt.jarvis.kmm",
"module": "kmm-infra",
"version": "1.0.0-SNAPSHOT",
"attributes": {
"org.gradle.status": "integration"
}
},
"createdBy": {
"gradle": {
"version": "7.4.2"
}
},
"variants": [
{
"name": "iosArm64ApiElements-published",
"attributes": {
"artifactType": "org.jetbrains.kotlin.klib",
"org.gradle.category": "library",
"org.gradle.usage": "kotlin-api",
"org.jetbrains.kotlin.native.target": "ios_arm64",
"org.jetbrains.kotlin.platform.type": "native"
},
"dependencies": [
{
"group": "org.jetbrains.kotlin",
"module": "kotlin-stdlib-common",
"version": {
"requires": "1.8.0"
}
}
],
"files": [
{
"name": "kmm-infra.klib",
"url": "kmm-infra-iosarm64-1.0.0-SNAPSHOT.klib",
"size": 6396,
"sha512": "2ebdb65f7409b86188648c1c9341115ab714ad5579564ce4ec0ee7fb6e0286351f01d43094bc7810d59ab1c4d4fa7887c21ce53bc087c34d129309396ceb85a5",
"sha256": "056914503154535806165c132df52819aedcc93a7b1e731667a3776f4e92ff79",
"sha1": "c43ed6cb8b5bf3f40935230ce3a54b2f27ec1d6a",
"md5": "d79166eda9f4bf67f5907b368f9e9477"
}
]
},
{
"name": "iosArm64MetadataElements-published",
"attributes": {
"artifactType": "org.jetbrains.kotlin.klib",
"org.gradle.category": "library",
"org.gradle.usage": "kotlin-metadata",
"org.jetbrains.kotlin.native.target": "ios_arm64",
"org.jetbrains.kotlin.platform.type": "native"
},
"dependencies": [
{
"group": "org.jetbrains.kotlin",
"module": "kotlin-stdlib-common",
"version": {
"requires": "1.8.0"
}
}
],
"files": [
{
"name": "kmm-infra-iosarm64-1.0.0-SNAPSHOT-metadata.jar",
"url": "kmm-infra-iosarm64-1.0.0-SNAPSHOT-metadata.jar",
"size": 5176,
"sha512": "fa828f456c3214d556942105952cb901900a7495f6ce6030e4e65375926a6989cd1e7b456f772e862d3675742ce2678925a0a12a1aa37f4795e660172d31bbff",
"sha256": "c4de0db2b60846e3b0dbbd25893f3bd35973ae790696e8d39bd3d97d443a7d4c",
"sha1": "e59036a081663f5c5c9f96c72c9c87788233c8bc",
"md5": "9293e982f84b623a5f0daf67c6e7bb33"
}
]
}
]
}
iOS 平台编译产物
我们其实可以通过上面 iOS 平台 .module 文件看到一些描述,有 metadata.jar
和 .klib
├── kmm-infra-iosarm64
│ ├── 1.0.0-SNAPSHOT
│ │ ├── kmm-infra-iosarm64-1.0.0-SNAPSHOT-metadata.jar
│ │ ├── kmm-infra-iosarm64-1.0.0-SNAPSHOT-sources.jar
│ │ ├── kmm-infra-iosarm64-1.0.0-SNAPSHOT.klib
│ │ ├── kmm-infra-iosarm64-1.0.0-SNAPSHOT.module
│ │ ├── kmm-infra-iosarm64-1.0.0-SNAPSHOT.pom
│ │ └── maven-metadata-local.xml
│ └── maven-metadata-local.xml
└── kmm-infra-iosx64
├── 1.0.0-SNAPSHOT
│ ├── kmm-infra-iosx64-1.0.0-SNAPSHOT-metadata.jar
│ ├── kmm-infra-iosx64-1.0.0-SNAPSHOT-sources.jar
│ ├── kmm-infra-iosx64-1.0.0-SNAPSHOT.klib
│ ├── kmm-infra-iosx64-1.0.0-SNAPSHOT.module
│ ├── kmm-infra-iosx64-1.0.0-SNAPSHOT.pom
│ └── maven-metadata-local.xml
└── maven-metadata-local.xml
metadata.jar
,主要存放了.knm
.klib
,也存放了 metadata.jar 中相同的内容,除此以外还有 ir,方便编译器后端继续编程机器码- 如果不了解 ir 是什么,可以参考我之前写的 Kotlin Compiler】IR 介绍
.knm 和 .klib 是什么?后文会具体介绍
三、.klib 和 .knm 文件
- klib 的文件结构是怎样的?
- .knm 是什么文件?为什么只能用 IDEA 浏览?
klib 文件结构
klib 指 Kotlin Library
klib
├── ir
│ ├── bodies.knb
│ ├── debugInfo.knd
│ ├── files.knf
│ ├── irDeclarations.knd
│ ├── signatures.knt
│ ├── strings.knt
│ └── types.knt
├── linkdata
│ ├── module
│ ├── package_com
│ │ └── 0_com.knm
│ ├── package_com.jarvis
│ │ └── 0_jarvis.knm
│ ├── package_com.jarvis.kmm
│ │ └── 0_kmm.knm
│ ├── package_com.jarvis.kmm.infra
│ │ └── 0_infra.knm
│ └── root_package
│ └── 0_.knm
├── manifest
├── resources
└── targets
└── ios_arm64
├── included
├── kotlin
└── native
.knm 的生成过程
knm 指 kotlin native metadata
- .kt 经过编译器 frontend, 生成 kotlinIr
- 经过 protobuf 序列化后,生成 .knm 文件,这也解释了 vim 打开是乱码的原因
- .knm 通过反序列化可以得到 KotlinIr
- KotlinIr 通过反编译可以得到代码的细节,这正是在 IDEA 里能看到 .knm 是什么的原因
使用安装 Kotlin Plugin 的 IDEA 查看 knm 文件
使用 vim 查看 knm 文件
四、iOS 和 KMM 库的关系
iOS 中的依赖库是一组 .h 和二进制文件,所以 KMM 库最终一定要转成 .h 和二进制文件。
KMM 中,iOS 平台的编译产物是 klib问题:
- Kotlin 是怎样依赖并调用 iOS Objective-C 库的?
- iOS 是如何使用 KMM 库的?
为了解释上面的两个问题,需要了解 KMM 和 OC 互操作的机制(互相调用),以及 klib 是如何打包
OC 互操作流程
- Copy iOS 工程中需要用到的 .h 文件(此处也可以直接在 KMM 工程中通过 Cocoapods 插件直接依赖 pod 库)
- .h 文件通过
cinterop
工具生成 klib,由于 kotlin 不认识 oc 的 .h,所以需要通过 klib 将 .h 转成 kotlin 认识的形式后才能调用 - 将开发完成的 kotlin 代码编译打包,通过
fatFramework
工具输出最终 .h 和二进制文件 - iOS 依赖 Umbrella.h 和二进制文件,此流程已经走到 iOS 原生端,和 KMM 无关了
FatFrameWork 流程
- KMM 工程打包 klib 并上传
- KMM_Umbrella (依赖了很多 KMM 库的全家桶工程) 工程拉取 klib 依赖
- 执行 iosFatFramework 任务,输出最终 framework.h 和二进制文件
- klib 中的 ir 通过 kotlin 编译器后端,编译成对应平台的二进制文件
- 链接
- 合并不同架构的二进制文件,比如 iosArm64 iosX64,具体可参考【mac】lipo命令详解
- 合并头文件
- 创建 .modulemap 文件,具体细节可以参考 理解 iOS 中的 Modules
- 生成 info.plist ,此文件是对 framework 的描述清单文件
- 合成 DSYM( Debugger Symbols) 文件
最终输出结构如下
fat-framework
└── debug
└── KMMUmbrellaFramework.framework
├── Headers
│ └── KMMUmbrellaFramework.h
├── Info.plist
├── KMMUmbrellaFramework
└── Modules
└── module.modulemap
总结
- 通过在 Common 层定义
expect
接口,生成 .knm,以及关联具体平台信息的 .module - 在具体平台通过
actual
实现接口,生成 .klib/.aar/.jar - Android 平台比较特殊,因为 Kotlin 以前只能编译成 JVM 字节码,不存在 ir 概念,K2 Compiler 出现后,统一抽象了编译流程,使得 JVM 也有了自己的编译器后端,也可以通过 IR 编译为 JVM 字节码
- iOS 平台通过 .klib 存放 ir,然后经过编译器后端打成 iOS 可以使用的 .framework
- 将对应产物接入到对应平台工程
通过对 KMM 编译产物的探索,能让我们更好地理解 KMM 是如何实现跨平台的。
参考
来源:juejin.cn/post/7214412608400212028