一、什么是KMM?
Kotlin Multiplatform Mobile (KMM) 是一个 SDK,旨在简化跨平台移动应用程序的创建。在 KMM 的帮助下,您可以在 iOS 和 Android 应用程序之间共享通用代码,并仅在必要时编写特定于平台的代码。
KMM 用纯 Kotlin 编写一次代码,即可在 iOS 和 Android 上运行,开发应用的公共业务逻辑只需要编写一次。这显著减少了为不同平台编写和维护相同代码所花费的时间。在 Jenkins 上一次构建可以产出 aar、framework 和 klib,Android 依赖 aar,iOS 依赖 framework,性能与原生一致。当然,也可以使用 KMM 依赖 klib 来开发 Android 和 iOS 应用。
二、KMM项目架构
KMM 项目架构主要分为原生系统层、Android/iOS 业务 SDK 层、KMM SDK 层、KMM 业务逻辑 SDK 层、iOS sdkframework 层以及 Android/iOS App 层。
原生系统层涉及一些需要分开实现的平台特性,例如读取文件、打印日志或使用摄像头等。
Android/iOS 业务 SDK 层主要包括一些现有的 Android/iOS SDK。当需要直接依赖现有 SDK 来开发 KMM 时,可以在 commonMain 中使用 expect 声明接口,然后在 androidMain 和 iosMain 中通过 actual 分别依赖现有 SDK 实现。这样就能利用已有 SDK,后续也可以保持接口不变,直接使用 KMM 实现 SDK,例如 alog 和 PlatformMMKV。
KMM SDK 层是将如 alog、PlatformMMKV 等功能封装成 SDK,供其他 KMM 模块(如业务模块)使用。
KMM 业务逻辑 SDK 层包含具体业务的逻辑模块,例如登录逻辑、获取首页列表逻辑或查看首页列表数据详情等。
iOS sdkframework 层是为了解决包大小问题。当 Kotlin/Native 构建一个 framework 时,产物是二进制文件,其中包含了 Kotlin/Native 的基础库和 Runtime,这会使包大小增加约 1MB 以上。而且,多个 Kotlin/Native 构建的 framework 不会共享基础库,导致每一个 framework 都会增加约 1MB。为了避免包过大,通常统一构建一个 framework。
App 层中,Android 的依赖无变化,可以依赖 aar 或 jar;iOS 则依赖 sdkframework,这样 iOS 包大小只增加约 1MB。当然,如果依赖了其他库如 ktor 网络库,包也会变大。为了避免这个问题,也可以不依赖 ktor,而是直接依赖现有的网络库来实现一个 KMM SDK。
三、使用expect/actual编写平台特定的代码
以打印日志为例,打造一个 alog 日志 SDK。在 commonMain 中定义 IALog 接口,声明 fun v 函数(其他函数类似),并定义 expect ALogImpl 类来实现平台特性打印日志。
interface IALog {
fun v(tag: String, message: String)
// ... 其他函数
}
expect class ALogImpl(): IALog
在 androidMain 中实现 ALogImpl。
import android.util.Log
actual class ALogImpl actual constructor() : IALog {
override fun v(tag: String, message: String) {
Log.v(tag, message)
}
// ... 其他实现
}
在 iosMain 中实现 ALogImpl。
import platform.Foundation.NSLog
internal actual class ALogImpl actual constructor(): IALog {
override fun v(tag: String, message: String) {
NSLog("[$tag] $message")
}
// ... 其他实现
}
这样,我们就使用 KMM 实现了一个 alog 日志 SDK。
四、依赖现有的Android/iOS SDK开发KMM SDK
alog 的实现过于简单,直接使用了 android.util.Log 和 platform.Foundation.NSLog。如果希望依赖现有的 Android/iOS SDK,例如 Android 使用 mars-xlog、iOS 使用 CocoaLumberjack,可以按以下方式实现。
Android 的实现变化不大,只需依赖 mars-xlog 即可。
implementation("com.tencent.mars:mars-xlog:1.2.6")
import com.tencent.mars.xlog.Log
actual class ALogImpl actual constructor() : IALog {
override fun v(tag: String, message: String) {
Log.v(tag, message)
}
// ... 其他实现
}
在 iOS 实现中依赖 CocoaLumberjack,需要使用 native.cocoapods 插件。
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
}
cocoapods {
// ... 配置
frameworkName = "alog"
pod("CocoaLumberjack")
}
通过 cinterop,一些 Gradle Task 会自动生成头文件供 iosMain 使用,例如生成 alog-cinterop-CocoaLumberjack.klib 包含相关文件。
import cocoapods.CocoaLumberjack.*
internal actual class ALogImpl actual constructor(): IALog {
private val dLog = DDLog
override fun v(tag: String, message: String) {
dLog.log(asynchronousLog, toMessage(tag, "[$tag] $message", DDLogLevelVerbose, DDLogFlagVerbose))
}
private fun toMessage(tag: String, message: String, level: DDLogLevel, flag: DDLogFlag): DDLogMessage {
return DDLogMessage(message, level, flag, 0, "", null, 0, tag, 0, null)
}
// ... 其他实现
}
为了方便 Android/iOS App 使用,可以添加一个 ALog.kt 类。
/**
* Android App使用 ALog.i(tag, message)
*/
val ALog: IALog by lazy { ALogImpl() }
/**
* iOS App使用ALogKt.i(tag, message)
*/
fun d(tag: String, message: String) = ALog.d(tag, message)
这样,alog 就完成了依赖现有的 Android/iOS SDK(mars-xlog 和 CocoaLumberjack)开发 KMM SDK。
五、声明Android/iOS公共接口以及独有接口
在 commonMain 中使用 expect 修饰声明公共接口。
expect interface IALog {
fun v(tag: String, message: String)
// ... 其他公共方法
}
在 iosMain 中使用 actual 修饰来实现真正的接口。
actual interface IALog {
actual fun v(tag: String, message: String)
// ... 其他实际方法
}
在 androidMain 中使用 actual 修饰来实现真正的接口,带 actual 修饰的方法为 Android/iOS 公共方法,不带 actual 修饰的方法为 Android 独有接口(即 Android 有这个接口而 iOS 没有)。
actual interface IALog {
actual fun v(tag: String, message: String)
// ... 其他公共方法
fun v(tag: String, format: String, vararg args: Any?) // Android 独有方法
}
这样,Android 就可以使用 fun v(tag: String, format: String, vararg args: Any?) 函数,而 iOS 没有这个函数。这种方式的好处是,通常 SDK 在 commonMain 中会定义一套公共接口,当 Android 或 iOS 有一些独有接口时,就可以灵活声明。类似地,data class 也可以这样使用。
六、为iOS统一构建成一个framework
为了避免 Kotlin/Native 构建 framework 时包过大,通常统一构建一个 framework(这里称为 sdkframework)。有2种构建方式:一是本地构建,写一个 sdkframework 项目依赖其他模块的 klib 包来构建;二是在构建系统上构建依赖其他模块的 klib 包,业务直接 pod sdkframework 即可。第一种方案比较灵活,版本号可以用脚本控制,但要求开发人员的电脑都配置 KMM 开发环境。第二种方案业务接入更简单,与 iOS 原生开发的 SDK 一样,无需 KMM 环境,主要问题是各个业务依赖 klib 的版本不一致,导致构建多个版本的 sdkframework。这时需要用不同分支构建不同业务的 sdkframework,版本号加后缀来区别,例如 1.0.0-love、1.0.0-like。
6.1 sdkframework模块的iosMain需要有一个kotlin文件
如果 iosMain 没有 kotlin 文件,将无法生成 iOS framework。为此,可以添加一个文件,例如 SDKTest.kt。
// 加个类,避免Framework没生成
class SDKTest {
fun test() {
// 空实现
}
}
6.2 生成头文件sdkframework.h时带上注释
生成头文件 sdkframework.h 时,如果需要带上注释,需要在 gradle 中添加 Task。
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
compilations.get("main").kotlinOptions.freeCompilerArgs += "-Xexport-kdoc"
}
6.3 使用export导出依赖模块到头文件
sdkframework 依赖了 utils、alog、PlatformMMKV、business 等模块,需要添加 export,将这些模块的类和方法导出到 sdkframework.h 头文件中,这样 iOS App 才可以使用这些模块的类和方法。
val iosX64 = iosX64()
val iosArm64 = iosArm64()
targets {
configure(listOf(iosX64, iosArm64)) {
binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java) {
export(project(":utils"))
export(project(":alog"))
export(project(":PlatformMMKV"))
export(project(":business"))
}
}
}
6.4 处理sdkframework本地依赖模块的pod问题
当 sdkframework 依赖 utils、alog、PlatformMMKV、business 等模块源码构建 framework 时,如果这些模块使用了 pod,那么 sdkframework 也需要添加相同的 pod。例如,如果 PlatformMMKV 使用了 pod("MMKV", "1.2.8"),那么 sdkframework 也要 pod("MMKV", "1.2.8")。为了避免这个问题,可以先将 utils、alog、PlatformMMKV、business 模块在构建系统上构建成 klib,然后让 sdkframework 依赖各个模块的 klib。
对于本地构建,在 iOS App 本地依赖构建 sdkframework 时,要将依赖项正确导入 Kotlin/Native 模块,Podfile 必须包含 use_modular_headers! 或 use_frameworks! 指令。具体可参考相关文档。当然,如果在构建系统上构建则不需要这些指令。
七、参考链接
- 本文地址:https://www.cnblogs.com/liqw/p/15416758.html
- kmm-getting-started:https://kotlinlang.org/docs/kmm-getting-started.html
- Multiplatform programming:https://kotlinlang.org/docs/multiplatform.html
- KMM 求生日记二:Kotlin/Native 被踩中的坑:https://mp.weixin.qq.com/s/e3k5JcxG1FvGlNkOyjNIFw
- KNDemo:https://github.com/River418/KNDemo
- 源码地址:https://github.com/libill/kmmApp