注册

初探 Kotlin Multiplatform Mobile 跨平台原理




一、背景



本文会尝试通过 KMM 编译产物理解一套 kt 代码是如何在多个平台复用的。


KMM 发流程简介


我以开发一个 KMM 日志库为例,简单介绍开发流程是什么:



  1. 在 CommonMain 定义接口,用 expect 关键字修饰,表示此接口在不同平台的实现不一样。
  2. 在具体平台实现接口,并用 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)
}


  1. 编译、打包、发布

publish_artifacts.png



  1. 依赖具体平台仓库

    1. 如果宿主为 Android App,则依赖对应的 kmm-infra-android
    2. 如果宿主为 iOS App,需要现将 kmm-infra-iosarm64 打包成 Framework,然后 iOS 依赖 Framework
    3. 如果宿主为 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 文件




  1. klib 的文件结构是怎样的?
  2. .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


kt2knm.svg



  1. .kt 经过编译器 frontend, 生成 kotlinIr
  2. 经过 protobuf 序列化后,生成 .knm 文件,这也解释了 vim 打开是乱码的原因
  3. .knm 通过反序列化可以得到 KotlinIr
  4. KotlinIr 通过反编译可以得到代码的细节,这正是在 IDEA 里能看到 .knm 是什么的原因

使用安装 Kotlin Plugin 的 IDEA 查看 knm 文件


idea_knm.png


使用 vim 查看 knm 文件


vim_knm.png


四、iOS 和 KMM 库的关系



iOS 中的依赖库是一组 .h 和二进制文件,所以 KMM 库最终一定要转成 .h 和二进制文件。
KMM 中,iOS 平台的编译产物是 klib


问题:



  1. Kotlin 是怎样依赖并调用 iOS Objective-C 库的?
  2. iOS 是如何使用 KMM 库的?

为了解释上面的两个问题,需要了解 KMM 和 OC 互操作的机制(互相调用),以及 klib 是如何打包



OC 互操作流程


interop_ios.png



  1. Copy iOS 工程中需要用到的 .h 文件(此处也可以直接在 KMM 工程中通过 Cocoapods 插件直接依赖 pod 库)
  2. .h 文件通过 cinterop 工具生成 klib,由于 kotlin 不认识 oc 的 .h,所以需要通过 klib 将 .h 转成 kotlin 认识的形式后才能调用
  3. 将开发完成的 kotlin 代码编译打包,通过 fatFramework 工具输出最终 .h 和二进制文件
  4. iOS 依赖 Umbrella.h 和二进制文件,此流程已经走到 iOS 原生端,和 KMM 无关了

FatFrameWork 流程


assemble_ios.png



  1. KMM 工程打包 klib 并上传
  2. KMM_Umbrella (依赖了很多 KMM 库的全家桶工程) 工程拉取 klib 依赖
  3. 执行 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


总结


conclusion.png



  1. 通过在 Common 层定义 expect 接口,生成 .knm,以及关联具体平台信息的 .module
  2. 在具体平台通过 actual 实现接口,生成 .klib/.aar/.jar
  3. Android 平台比较特殊,因为 Kotlin 以前只能编译成 JVM 字节码,不存在 ir 概念,K2 Compiler 出现后,统一抽象了编译流程,使得 JVM 也有了自己的编译器后端,也可以通过 IR 编译为 JVM 字节码
  4. iOS 平台通过 .klib 存放 ir,然后经过编译器后端打成 iOS 可以使用的 .framework
  5. 将对应产物接入到对应平台工程

通过对 KMM 编译产物的探索,能让我们更好地理解 KMM 是如何实现跨平台的。


参考



作者:ZzT
来源:juejin.cn/post/7214412608400212028

0 个评论

要回复文章请先登录注册