一、什麼是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