Kotlin/Native KMM專案架構

傳說之美(libill)發表於2021-10-17

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

2、kmm-getting-started

3、Multiplatform programming

4、KMM 求生日記二:Kotlin/Native 被踩中的坑

5、KNDemo

相關文章