1. 简介
1.1 什么是 KMM?
KMM 全称:Kotlin Multiplatform Mobile,是一个用于跨平台移动开发的软件开发工具包(SDK),可以在iOS和Android应用程序中使用相同的业务逻辑代码。它和 Kotlin Native(简称 KN)有一定联系,但 KMM 主要面相移动端开发,即:Android、iOS、Web,而 KN 则主要面相 Linux、macOS、Windows 等。当然,KMM 在 iOS 平台的实现,离不开 Kotlin Native,Kotlin 代码最终会在 iOS 工程中生成一套 Framework 库,可供 Objective-C、Swift 进行调用
KMM 宗旨是使用 Kotlin 语言和技术栈,开发一套可以在多平台之间共享的代码库,用来构建统一的代码逻辑,而不用针对各个平台都去实现自己的一套,从而导致人力的浪费。
这里引用 Kotlin 官网的一张图来说明 Kotlin 多平台的工作原理
1.2 KMM 是如何运行的?
KMM 的结构
- Common
- Android
- iOS-
Kotlin Multiplatform 项目一般分为Common Kotlin 代码和平台特定代码(Android/iOS),Common Kotlin 包括核心库和基本工具,可以依赖诸如 HTTP、序列化和协程管理等日常任务的库,里面编写的代码可以在所有平台上运行。
-
对于 Android,为了与平台进行交互,可以使用特定平台版本的 Kotlin(Kotlin/JVM、Kotlin/JS、Kotlin/Native),这些平台将 kotlin 代码编译成相应的字节码或者Dex。
-
对于 iOS,共享的 Kotlin 代码会通过 Kotlin/Native 编译为本机二进制文件,在应用中捆绑一个常规的 iOS 框架,通过 Framework 的方式导入项目。
iOS 的调用逻辑会稍显复杂,因为会设计到 kotlin 和 OC/Swift 的相互调用,在编译时会有一些中间代码。
Kotlin Native 内部使用 cinterop(Kotlin Native 的核心) 来对 Apple Framework 进行扫描,根据 Framework 中的 Headers(.h 文件)获取可以调用的类、方法、变量、常量及他们对应的类型,最终生成 klib 文件,而 klib 文件中包含着针对不同 CPU 架构所编译的二进制文件,以及可供 Kotlin Native 调用的 knm 文件,knm 文件类似 Jar 包中的 .class 文件,是被编译后的 Kotlin 代码,内部将 cinterop 扫描出来的 Objective-C 内容转换成了 Kotlin 对应的内容,以便 IDE 可以进行索引,最终在 KMM 模块中使用 Kotlin 代码进行调用。
对于 OC 调用 Kotlin 的流程官方文档中没有太多提及,使用的是 Kotlin Native 与 C/C++/Objective-C 的混编能力,具体可查看官方文档。
https://book.kotlincn.net/text/apple-framework.html
https://kotlinlang.org/docs/native-overview.html
-
对于 Common,适用于所有平台的通用业务逻辑,Android/iOS 使用 Kotlin 提供的机制进行共享。
1.3 与其他跨平台方案对比
近年来在跨平台解决方案方面进行了许多努力,一些流行的跨平台解决方案包括 React Native、weex、Flutter、Ajx 等。上述框架要么需要我们采用全新的框架,学习一门新的语言,要么具有桥接基础设施,这导致性能不如本地开发。这就是 Kotlin Multiplatform 与其他解决方案不同之处,因为它可以很好地与现有的本地代码/基础设施配合使用。此外,我们不需要经历学习新语言的曲线,而是可以利用已经熟悉 Kotlin 的现有 Android 开发人员。
KMM 有以下这些特点:
- 不涉及 UI:与其他框架不同,Kotlin Multiplatform 不涉及 UI,仅专注于业务逻辑。在 Kotlin Multiplatform 中,UI 仍将在各自的本地平台上编写。
- 无桥接 API:Kotlin Multiplatform 不需要任何桥接机制。例如,React Native 需要桥接 API 才能在本地和 React Native 之间进行通信。
- 无需引入新的引擎:Kotlin Multiplatform 在 Android 中直接使用 Kotlin,相当于本地编译,而 flutter 拥有渲染引擎,直接调用 OpenGL/Skia 的 API 进行绘制,另一个是 Dart 语言的 Runtime,两者都会增大包体积。
- 更好的工具支持:由于 Kotlin Multiplatform 由 JetBrains 开发,我们可以确信会有良好的工具支持。
- 使用现有语言:像 Flutter 和 React Native 这样的跨平台解决方案分别使用 Dart 和 JS,这些语言不被现有的本地移动平台所使用。
- 性能:在 Kotlin 中编写的共享代码会编译为不同的输出格式,以适应不同的目标:Android 会编译为 Java 字节码,iOS 会编译为本机二进制代码。因此,在执行此代码时,不会有额外的运行时开销,性能与本地应用程序相当。
1.4 如何使用 KMM
在开发中,实际上包括 2 个部分,1. 各个平台的代码;2. Common 下的公共逻辑。由于 KMM 运行在各平台时,实际上是翻译成了各平台专用的库,如:Android 上就会将共享模块编译成 Dalvik Bytecode 然后打包成 AAR 文件,而 iOS 上会打包成 Apple Framework,所以,除了编写公共逻辑的代码外,还需要一些平台相关的、不可共享的具体实现代码,而这些就必须利用各平台的 API 来实现。
举个例子,公共模块有一个统一的业务逻辑——获取手机型号,控制逻辑可以在 KMM 的 common 代码库中实现,且它并不关系具体的实现逻辑,而实际需要获取手机型号字符串的方法,Android 需要调用 android.os.Build.MODEL 获取,而 iOS 需要通过 UIDevice.current.model 来获取,类似的平台强相关功能,就需要在 KMM 中利用平台差异化代码实现。
KMM 中使用了 expect/actual 关键字完成了平台差异的逻辑统一。
比如需要实现一个 Logger 模块,需要在 Common 中声明相关 expect 方法(有点类似于 java 中的 interface 和 iOS 中的协议),expect 修饰的可以是object、class、function
在 AndroidMain 和 iosMain 中分别实现 getPlatform()
Android
iOS
这样在上层代码中,就可以使用 getPlatform() API 区分平台信息。
感兴趣的同学可以参考官方 Demo,一步一步学习开始上手 https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html
2. 集成
2.1. 集成流程
- KMM 中 Android 的集成流程与普通 module 差异不大,可以通过 aar 和 Maven两种方式依赖,依赖时需要注意 gradle 和 Kotlin 版本,因为 KMM 基于高版本 Kotlin 进行开发,对 Kotlin 和 gradle 版本要求都比较高。
2.2. 三方依赖
- KMM 依赖类型
KMM 的依赖根据平台分为三类,分别是 Common 依赖、Android 依赖、iOS 依赖, Android 的依赖就是我们平常使用的这些,例如:OKHttp、Gson、Glide 等等; Common 依赖顾明思议,是用于通用逻辑的,这种依赖只能使用基于最标准的 Kotlin 底层能力(不可以耦合 JVM、JS)构建,相对于Android/iOS 依赖,这些三方库尚且较少,只有官方出品的一些常用库(比如:json 解析、网络、数据库),且功能不够强大,好处是这些三方库已经适配了两个平台的差异,可以直接构建公共逻辑。
- Common 依赖
官方 JSON 解析库:https://github.com/Kotlin/kotlinx.serialization
HTTP 请求库:https://github.com/ktorio/ktor
这些库的依赖也非常简单,和普通的 Gradle 依赖类似,只需要在 KMM 模块根目录的 build.gradle.kts 文件中添加即可,如下图所示,在 commonMain 变量后面的闭包中,新建一个 dependencies 闭包,即可以按照常规的 Gradle 依赖形式,添加 serialization 的 Common 依赖
-
Android 依赖
类似于 Common,直接在 androidMain 中添加依赖
与 Common 的区别在于,这属于 Android 的依赖,只能在AndroidMain 中使用,流程也和我们原始的开发类似
- iOS 依赖
iOS 可直接导入 Framework 或者使用 cocopod,可以参考 https://coderyuan.com/2021/05/28/KMM-2/
2.4. 版本管理
- 在 gradle 8.0 中 编译脚本默认使用 kts,kts 通过 DSL 的方式可以和 Kotlin 进行混编,可以更好的安排版本依赖。可查考 https://medium.com/better-programming/make-gradle-dependencies-management-better-d04e48168244
2.5. 问题
- 环境问题
环境是我们要面对的一个很重要的问题,因为我们依赖于 ajx(甚至没有源码),很多代码也过于老旧,很久没有进行过升级适配,同时还有编译时 gradle 相关 API,导致升级 gradle 时遇到一些问题,包括 Kotlin、AGP、gradle 最低版本要求;Android Studio jvm 环境要求;第三方 SDK 对环境的要求等等
针对这些环境问题,我们在使用的过程中都一一进行了解决,详细的解决方案在下一节中。无论是使用哪种解决方案,kotlin、gradle 的升级都是势在必行的,因为 KMM 尚处在一个 Beta 阶段,尽管API 已经稳定,但是仍旧有一些问题,而这些问题都需要在新版本的修复,我们如果停留在某个版本,随着我们使用的越多问题堆积将会越多,影响后面的大范围使用。
- 低版本 Memory Model (https://kotlinlang.org/docs/native-memory-manager.html)
在 kotlin 1.7.20 之前,Kotlin Native 尚不完善,在使用 var 时如果不添加 ThreadLocal 注解,在初始化之后无法更改其值。
2.6. 解决方案
- 升级主项目
◦ 旧 bundle 升级
bundle 的升级主要是针对主项目,在没有使用到 KMM 的项目,理论上可以不用升级,gradle 主要用于编译时期,高版本也兼容低版本的 aar。
主项目中 kotlin 升级到 1.8.20,gradle 和 AGP 进行相应的升级,(gradle 8.0 版本会移除 transform api,ajx 使用到了 transform 来进行数据收集,升级 8.0 时需要单独适配,目前 7.x 已经开始标记为弃用)
- 未知问题
对于一个大体量的项目来说,升级 gradle 一直是一个危险操作,为了方便,我们会在编译时对 App 做很多改动,如果没有很好的适配对于编译和打包会造成不可预见性的问题,因为工期较紧,我们并没有采用这种方式来解决环境适配的问题。
降低 KMM
依赖降低版本
既然 gradle 的升级是 KMM 引入的,那么可不可以降低 KMM 的版本?根据上面的 gradle 版本对照,答案是肯定的,KMM 在 2 年前就开始了开发,也经历过低版本的时期,我们根据上面图表中的版本对 KMM 进行降级处理,最终打出的 aar 可以在主工程不影响的情况下打包。
- 不能使用新特性
kotlin 团队在22 年 10 才发布了 Beta 版本,使用旧的版本仍可能带来一些之前的问题,比如,在使用 Kotlin 1.7.20 之前的版本开发 KMM 项目时,需要使用 freeze、@SharedImmutable、@ThreadLocal 这样的语法来保证多线程之前的共享状态。
另外,Top Level 属性在 Legacy Memory Management 上也有初始化问题(by lazy 可能造成 iOS App 崩溃)
3. 收益
- 代码量降低
- 双端拉齐
- 效率提升
4. 未来展望
- 环境升级
现在 Android 打包还是在低版本,iOS 也需要一些脚本做额外处理,首先要处理的任务就是把环境提升到一致的情况,这样最终让代码也保持一致。
- 底层解耦
KMM 作为一个逻辑层,仍然需要依赖很多基础层模块,比如 log、网络、json 解析、文件存储等等模块,这里我们需要下沉更多基础模块,为未来的升级做准备。
逻辑拉齐
H5 逻辑
网络请求
底层 SDK 能力拉齐
...
参考
https://medium.com/better-programming/make-gradle-dependencies-management-better-d04e48168244
https://coderyuan.com/categories/Kotlin/