一、什么是KMM?
Kotlin Multiplatform Mobile ( KMM ) 是一個 SDK,旨在簡化跨平台移動應用程序的創建。在 KMM 的幫助下,您可以在 iOS 和 Android 應用程序之間共享通用代碼,並僅在必要時編寫特定於平台的代碼。
KMM用純Kotlin編寫一次代碼,即可在iOS和Android上運行,開發應用的公共業務邏輯只需要編寫一次。KMM減少了為不同平台編寫和維護相同代碼所花費的時間。在Jenkins上一次構建可以產出aar、framework、klib,Android依賴aar,iOS依賴framework,性能與原生一致。當然可以使用KMM依賴klib開發Android、iOS應用。
二、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模塊(business)使用。
KMM業務邏輯SDK層:具體業務的邏輯模塊,比如登錄邏輯、獲取首頁列表邏輯、查看首頁列表數據詳情等。
iOS sdkframework層:Kotlin/Native構建一個framework時,產物是二進制,也包含了Kotlin/Native的基礎庫、Runtime,會使包大小增加1M+左右,而且多個Kotlin/Native構建的framework不會共享基礎庫導致每一個framework都會增加1M+,為了避免包過大,統一構建一個framework。
App層:Android的依賴無變化,依賴aar或者jar;iOS依賴sdkframework,這樣iOS包大小只增加1M+。當然如果依賴了一些庫如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包含1_CocoaLumberjack.knm。
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)開發alog KMM SDK。
五、聲明Android/iOS公共接口以及獨有接口
用expect修飾commonMain中聲明公共的接口
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就可以使用fun v(tag: String, format: String, vararg args: Any?)函數,而iOS沒有這個函數。好處是通常一些SDK在commonMain中會定義一套公共接口,有時候Android或iOS有一些獨有接口,就可以用這種方式聲明。同理data class也是可以這樣使用。
六、為iOS統一構建成一個framework
為了避免Kotlin/Native構建framework時包過大,統一構建一個framework,下面把包名稱為sdkframework。這里提一下幾個值得注意的問題。有2種方式構建:1、本地構建,寫一個sdkframework項目依賴其他模塊的klib包,來構建sdkframework。2、構建系統上構建依賴其他模塊的klib包構建,業務直接pod sdkframework即可。第1種方案比較靈活,版本號可以寫腳本控制,但是要求開發人員使用的電腦都要配置KMM開發環境。第2種方案業務接入更加簡單,跟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.h頭文件中
sdkframework依賴了utils、alog、PlatformMMKV、business,需要添加export,把這幾個模塊的類和方法導出到sdkframework.h頭文件中,這樣iosApp才可以使用這幾個模塊的類和方法。
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也要pod,以klib依賴可避免該問題
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即可。
6.5 use_frameworks! 和 use_modular_headers!
上面說到的第1點本地構建,在iosApp本地依賴構建sdkframework時,要將依賴項正確導入 Kotlin/Native 模塊,Podfile必須包含use_modular_headers! 或 use_frameworks! 指令,查看文檔鏈接。當然,如果是第2點構建系統上構建則不需要使用這2個指令。
源碼地址:https://github.com/libill/kmmApp
七、參考鏈接:
1、本文地址:https://www.cnblogs.com/liqw/p/15416758.html
4、KMM 求生日記二:Kotlin/Native 被踩中的坑
5、KNDemo