KMM初探與編譯過程詳解

1.簡介

KMM,即Kotlin Multiplatform Mobile,是由Kotlin發(fā)布的移動端跨平臺框架。相比于其他跨平臺框架,KMM是原生UI+邏輯共享的理念,共享重復(fù)邏輯性的工作來提升開發(fā)效率的同時,保持原生執(zhí)行效率與UI特性。所以KMM并不會替代Android和iOS的原生開發(fā), 而是提倡將共有的邏輯部分抽出,由KMM封裝成Android(Kotlin/JVM)的aar和iOS(Kotlin/Native)的framework,再提供給View層進行調(diào)用,從而節(jié)約一部分的工作量。
我們先來比較幾種當(dāng)前流行的跨平臺框架:


截屏2022-11-18 15.07.18.png

主要說兩點:

  • 由于KMM的本質(zhì)就是原生App,跨平臺共享的內(nèi)容都是在編譯期進行的處理,所以在性能方面可以說是不受影響。其他的跨平臺方案,其性能多多少少都會受到影響且會增加包體積。
  • Flutter為代表的自帶渲染引擎實現(xiàn)UI框架在開發(fā)效率上是更高的。而KMM主要實現(xiàn)的是共享邏輯,UI層的實現(xiàn)還是建議平臺各自去處理,所以開發(fā)效率上來說,KMM優(yōu)于原生開發(fā),但不如Flutter。不過由于Android的官方語言就是Kotlin,對于Android開發(fā)來說,KMM的加持更像是一種贈送能力,幾乎可以無成本的進行KMM開發(fā)。

另外,今年十月初Android 官方宣布 Jetpack開始要支持KMM了,意味著KMM已經(jīng)得到了官方的支持。目前CollectionsDataStore 已經(jīng)可以通過依賴 -dev01 版本在多平臺上使用,加上KMM已經(jīng)到了Beta的階段,到了我們可以進行大膽嘗試的時候了。
因此本文將從準(zhǔn)備工作,結(jié)構(gòu)介紹,Demo示例和編譯過程詳解來對KMM框架進行說明。

2.準(zhǔn)備工作

2.1.安裝必要的工具和插件

  • 1.安裝或更新AndroidStudio和Xcode到最新版
  • 2.JDK
  • 3.Kotlin Multiplatform Mobile plugin:從AndroidStudio下載KMM插件,如圖所示:
image.png
  • 4.Kotlin plugin:更新Kolin Plugin到最新版本,如圖所示:
WeChat8547fb45aff7ca8206906cb36551da18.png

2.2.檢查環(huán)境配置

  • 1.打開Terminal使用HomeBrew安裝以下工具:

brew install kdoctor

  • 2.執(zhí)行kdoctor:

kdoctor

如果你的環(huán)境配置有問題,那么將會輸出相應(yīng)的信息,并以X作為標(biāo)記,大家可根據(jù)提示進行修改。
Tips:以上步驟可能需要科學(xué)上網(wǎng)。

3.KMM結(jié)構(gòu)說明

在嘗試KMM之前,我們先來了解一下KMM的基本結(jié)構(gòu)。
我們建個默認的KMM工程,看一下它的結(jié)構(gòu):

截屏2022-11-15 15.37.29.png

默認會生成androidApp,sharediosApp這三個子工程。其中androidAppiosApp為Android和iOS這兩個平臺的工程模塊,shared為共享邏輯模塊,供androidAppiosApp調(diào)用。我們打開根目錄的settings.gradle.kts

pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "My_Application"
include(":androidApp")
include(":shared")

會發(fā)現(xiàn)主項目只include了androidAppshared這兩個子項目,因為這兩個項目是Gradle項目,那么iOS的項目如何引用呢?我們摘抄一段官網(wǎng)的原文:

The iOS application is produced from an Xcode project. It's stored in a separate directory within the root project. Xcode uses its own build system; thus, the iOS application project isn't connected with other parts of the Multiplatform Mobile project via Gradle. Instead, it uses the shared module as an external artifact – framework.

意思是說iOS作為Xcode項目,儲存在根項目的另一個文件夾。Xcode有自己的編譯系統(tǒng),因此iOS項目并不依靠Gradle去和共享工程建立聯(lián)系,而是依靠將共享工程打包成framework供iOS項目使用。
我們可以看一下shared模塊編譯后的產(chǎn)物,如下圖所示:

WeChat370a3128d7aabf788a363c64bc81dd91.png

可以很清晰的看到生成的frameworkaar文件。
我們再來看看shared模塊都包含了什么:
截屏2022-11-15 16.15.40.png

  • 其中commonMain為公共模塊,該模塊的代碼與平臺無關(guān),是通過 expected關(guān)鍵字對一些api的聲明(聲明的實現(xiàn)在platform module中)。
  • androidMainiosMain分別為Android和iOS這兩個平臺,通過actual關(guān)鍵字在平臺模塊進行具體的實現(xiàn)。

我們繼續(xù)看看shared模塊的gradle文件都做了什么:

plugins {
    kotlin("multiplatform")
    id("com.android.library")
}

kotlin {
    android()
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
        }
    }
}

這是將這個KMM模塊編譯成Android aar和iOS framework的聲明。使用了Gradle編譯系統(tǒng)和KMM插件來進行實現(xiàn)。其中:

kotlin("multiplatform")

意味著引入KMM 插件。

id("com.android.library")

意味著生成一個Android aar,其配置用android {}進行了包裹:

android {
    namespace = "com.example.myapplication"
    compileSdk = 32
    defaultConfig {
        minSdk = 28
        targetSdk = 32
    }
}

iOS framework是使用Kotlin/Native進行編譯的,相應(yīng)的配置是用iosXXX{}進行了包裹:

listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
        }
    }

定義了輸出格式為framework,輸出名稱為shared
我們接著看kotlin還包含了什么:

kotlin {
//...
   sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
                implementation("io.ktor:ktor-client-core:$ktorVersion")
                implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
                implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")

            }
        }
        val androidMain by getting{
            dependencies {
                implementation("io.ktor:ktor-client-android:$ktorVersion")
            }
        }
//...
        val iosMain by creating {
//...
            dependencies {
                implementation("io.ktor:ktor-client-darwin:$ktorVersion")
            }
        }
//...
    }
}

在上一節(jié)我們提到的commonMain,androidMainiosMain就是sourceSets,其中commonMain是共享的邏輯,而androidMainiosMain是對Android和iOS共享邏輯的相應(yīng)實現(xiàn)。每一個sourceSet都可以有自己單獨的dependencies,如上面的代碼。支持分別引入implementation來實現(xiàn)各自的邏輯。另外Kotlin標(biāo)準(zhǔn)庫會被自動加到相應(yīng)的sourceSet中,無需重復(fù)引入。比方說上面的kotlinx-coroutines-core
之后,Android和iOS分別使用各自常規(guī)方式引入/調(diào)用aar或framework即可。
經(jīng)過以上的說明,我們大概了解了KMM,項目的架構(gòu),抄一張官網(wǎng)的圖進行總結(jié):

image.png

4.Demo示例

環(huán)境配置好了,我們就可以寫一個簡單的demo了。
打開Android Studio,F(xiàn)ile->New->New Project,選擇Kotlin Multiplatform App


WeChatbf589d9320e995bf372ce9655fd7fdea.png

點擊Next:


截屏2022-11-18 09.39.25.png

繼續(xù)點擊Next:
截屏2022-11-18 09.43.25.png

配置iOS項目時,將iOS framework distribution中選擇Regular framework,然后Finish。

至于為什么選擇Regular framework,官方文檔的描述如下:

We recommend using the regular framework for your first project, as this option doesn't require third-party tools and has less installation issues.
For more complex projects, you might need the CocoaPods dependency manager that helps handle library dependencies. To learn more about CocoaPods and how to set up an environment for them, see CocoaPods overview and setup.

大概意思是說Regular framework不需要三方工具,且有較少的安裝事項。對于更復(fù)雜的工程來說,可能需要使用CocoaPods dependency manage去管理libs。
對于我們的Demo工程來說,先使用Regular framework來進行說明。
在上個章節(jié)我們對于KMM工程的結(jié)構(gòu)進行了說明,我們進入shared模塊,看commonMain文件夾下Greeting的實現(xiàn):

class Greeting {
    private val platform: Platform = getPlatform()

    fun greeting(): String {
        return "Hello, ${platform.name}!"
    }
}

greeting()方法調(diào)用platform.name,Platform的實現(xiàn)如下:

interface Platform {
    val name: String
}

expect fun getPlatform(): Platform

Platform是個接口,使用expect關(guān)鍵字來聲明getPlatform(),再由Android和iOS通過使用actual關(guān)鍵字分別實現(xiàn):

Android:

class AndroidPlatform : Platform {
    override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

actual fun getPlatform(): Platform = AndroidPlatform()

iOS:

class IOSPlatform: Platform {
    override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getPlatform(): Platform = IOSPlatform()

最后我們看一下是如何調(diào)用的:

Android:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting(Greeting().greeting())
                }
            }
        }
    }
}

@Composable
fun Greeting(text: String) {
    Text(text = text)
}

iOS:

struct ContentView: View {
    let greet = Greeting().greeting()

    var body: some View {
        Text(greet)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

我們運行一下。

  • Android的話,在Android Studio上的configurations list中選擇androidApp,直接連接真機或開啟模擬器,點擊Run->Run 'androidApp'即可。
  • iOS的話,如果是第一次運行,需要打開Xcode,同意一些協(xié)議,然后回到Android Studio,configurations list中選擇iosApp,直接運行即可。

效果如下:


截屏2022-11-18 14.25.29.png

5.編譯過程詳解

接下來我們來看一下shared模塊是如何工作的。先關(guān)注一下shared模塊的gradle文件:

plugins {
    kotlin("multiplatform")
    id("com.android.library")
}

加載了multiplatform插件,并把此module作為lib輸出。

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
}

重點來了,這個是編譯雙端lib的關(guān)鍵。我們先看看android()是怎么編譯Android的aar的:

5.1.Android編譯過程

fun android() = android("android") { }

fun android(
        name: String = "android",
        configure: KotlinAndroidTarget.() -> Unit = { }
    ): KotlinAndroidTarget =
        configureOrCreate(
            name,
            presets.getByName("android") as KotlinAndroidTargetPreset,
            configure
        )

我們看到android()是創(chuàng)建了個KotlinAndroidTarget對象。
我們找到插件multiplatform的入口類:AbstractKotlinMultiplatformPluginWrapper:

abstract class AbstractKotlinMultiplatformPluginWrapper : KotlinBasePluginWrapper() {
    override fun getPlugin(project: Project): Plugin<Project> =
        KotlinMultiplatformPlugin()

    override val projectExtensionClass: KClass<out KotlinMultiplatformExtension>
        get() = KotlinMultiplatformExtension::class

    override fun whenBuildEvaluated(project: Project) {
        project.runMissingAndroidTargetProjectConfigurationHealthCheck()
        project.runMissingKotlinTargetsProjectConfigurationHealthCheck()
        project.runDisabledCInteropCommonizationOnHmppProjectConfigurationHealthCheck()
    }
}

返回一個KotlinMultiplatformPlugin對象。KotlinMultiplatformPlugin繼承Plugin,我們直接看apply()方法的實現(xiàn):

override fun apply(project: Project) {
//...

        setupDefaultPresets(project)
        customizeKotlinDependencies(project)
        configureSourceSets(project)
//...

截取重要的部分,看setupDefaultPresets(project)的實現(xiàn):

fun setupDefaultPresets(project: Project) {
        with(project.multiplatformExtension.presets) {
            add(KotlinJvmTargetPreset(project))
            add(KotlinJsTargetPreset(project).apply { irPreset = null })
            add(KotlinJsIrTargetPreset(project, isWasm = false).apply { mixedMode = false })
            add(
                KotlinJsTargetPreset(project).apply {
                    irPreset = KotlinJsIrTargetPreset(project, isWasm = false)
                        .apply { mixedMode = true }
                }
            )
            add(KotlinJsIrTargetPreset(project, isWasm = true).apply { mixedMode = false })
            add(KotlinAndroidTargetPreset(project))
            add(KotlinJvmWithJavaTargetPreset(project))

            // Note: modifying these sets should also be reflected in the DSL code generator, see 'presetEntries.kt'
            val nativeTargetsWithHostTests = setOf(LINUX_X64, MACOS_X64, MACOS_ARM64, MINGW_X64)
            val nativeTargetsWithSimulatorTests =
                setOf(IOS_X64, IOS_SIMULATOR_ARM64, WATCHOS_X86, WATCHOS_X64, WATCHOS_SIMULATOR_ARM64, TVOS_X64, TVOS_SIMULATOR_ARM64)

            HostManager().targets
                .forEach { (_, konanTarget) ->
                    val targetToAdd = when (konanTarget) {
                        in nativeTargetsWithHostTests ->
                            KotlinNativeTargetWithHostTestsPreset(konanTarget.presetName, project, konanTarget)
                        in nativeTargetsWithSimulatorTests ->
                            KotlinNativeTargetWithSimulatorTestsPreset(konanTarget.presetName, project, konanTarget)
                        else -> KotlinNativeTargetPreset(konanTarget.presetName, project, konanTarget)
                    }

                    add(targetToAdd)
                }
        }
    }

這個方法創(chuàng)建并添加了各個平臺的TargetPreset,看上去是用來配置各個平臺的編譯詳情的。我們先看看Android平臺KotlinAndroidTargetPreset的是如何創(chuàng)建的:

override fun createTarget(name: String): KotlinAndroidTarget {
        val result = KotlinAndroidTarget(name, project).apply {
            disambiguationClassifier = name
            preset = this@KotlinAndroidTargetPreset
            targetUnderConstruction = this
        }

        project.dynamicallyApplyWhenAndroidPluginIsApplied({ result })

        if (project.hasKpmModel) {
            mapTargetCompilationsToKpmVariants(result, PublicationRegistrationMode.AFTER_EVALUATE)
        }

        targetUnderConstruction = null

        return result
    }

創(chuàng)建了一個KotlinAndroidTarget對象,并執(zhí)行dynamicallyApplyWhenAndroidPluginIsApplied()方法:

internal fun Project.dynamicallyApplyWhenAndroidPluginIsApplied(
            kotlinAndroidTargetProvider: () -> KotlinAndroidTarget,
            additionalConfiguration: (KotlinAndroidTarget) -> Unit = {}
        ) {
            var wasConfigured = false

            androidPluginIds.forEach { pluginId ->
                plugins.withId(pluginId) {
                    wasConfigured = true
                    val target = kotlinAndroidTargetProvider()
                    androidTargetHandler().configureTarget(target)
                    additionalConfiguration(target)
                }
            }
//...
        }

看重點:

androidTargetHandler().configureTarget(target)

fun configureTarget(kotlinAndroidTarget: KotlinAndroidTarget) {
//...
        project.forEachVariant { variant ->
            val variantName = getVariantName(variant)
//...
            kotlinAndroidTarget.compilationFactory.create(variantName).let { compilation ->
                compilation.androidVariant = variant

                setUpDependencyResolution(variant, compilation)

                preprocessVariant(variant, compilation, project, kotlinOptions, kotlinConfigurationTools.kotlinTasksProvider)

                @Suppress("UNCHECKED_CAST")
                (kotlinAndroidTarget.compilations as NamedDomainObjectCollection<in KotlinJvmAndroidCompilation>).add(compilation)
            }

        }

        project.whenEvaluated {
            forEachVariant { variant ->
                val compilation = kotlinAndroidTarget.compilations.getByName(getVariantName(variant))
                postprocessVariant(variant, compilation, project, ext, plugin)

                val subpluginEnvironment = SubpluginEnvironment.loadSubplugins(project)
                subpluginEnvironment.addSubpluginOptions(project, compilation)
            }
//...
        }
//...
    }

先看preprocessVariant()的實現(xiàn):

    private fun preprocessVariant(
        variantData: BaseVariant,
        compilation: KotlinJvmAndroidCompilation,
        project: Project,
        rootKotlinOptions: KotlinJvmOptionsImpl,
        tasksProvider: KotlinTasksProvider
    ) {
//...
        tasksProvider.registerKotlinJVMTask(project, compilation.compileKotlinTaskName, compilation.kotlinOptions, configAction)
//...
    }

做了一些初始配置,并為tasksProvider注冊了KotlinJVMTask???code>postprocessVariant()的實現(xiàn):

private fun postprocessVariant(
        variantData: BaseVariant,
        compilation: KotlinJvmAndroidCompilation,
        project: Project,
        androidExt: BaseExtension,
        androidPlugin: BasePlugin
    ) {
//...
        val javaTask = variantData.getJavaTaskProvider()
        val kotlinTask = compilation.compileKotlinTaskProvider
//...
        wireKotlinTasks(project, compilation, androidPlugin, androidExt, variantData, javaTask, kotlinTask)
    }

創(chuàng)建了javaTask和kotlinTask,然后看wireKotlinTasks()的實現(xiàn):

    override fun wireKotlinTasks(
        project: Project,
        compilation: KotlinJvmAndroidCompilation,
        androidPlugin: BasePlugin,
        androidExt: BaseExtension,
        variantData: BaseVariant,
        javaTask: TaskProvider<out AbstractCompile>,
        kotlinTask: TaskProvider<out KotlinCompile>
    ) {
        val preJavaKotlinOutput = project.files(project.provider {
            mutableListOf<File>().apply {
                add(kotlinTask.get().destinationDirectory.get().asFile)
                if (Kapt3GradleSubplugin.isEnabled(project)) {
                    // Add Kapt3 output as well, since there's no SyncOutputTask with the new API
                    val kaptClasssesDir = Kapt3GradleSubplugin.getKaptGeneratedClassesDir(project, getVariantName(variantData))
                    add(kaptClasssesDir)
                }
            }
        }).builtBy(kotlinTask)

        val preJavaClasspathKey = variantData.registerPreJavacGeneratedBytecode(preJavaKotlinOutput)
        kotlinTask.configure { kotlinTaskInstance ->
            kotlinTaskInstance.libraries
                .from(variantData.getCompileClasspath(preJavaClasspathKey))
                .from(Callable { AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt) })

            kotlinTaskInstance.javaOutputDir.set(javaTask.flatMap { it.destinationDirectory })
        }
//...
    }

從方法命名我們可得知,它的目的是執(zhí)行KotlinTasks。我們看見kotlinTask是個TaskProvider對象,實際的操作在KotlinCompile里。KotlinCompile是個Task,其execute實現(xiàn)在父類AbstractKotlinCompile中:

    @TaskAction
    fun execute(inputChanges: InputChanges) {
        val buildMetrics = metrics.get()
        buildMetrics.measure(BuildTime.GRADLE_TASK_ACTION) {
//...
            executeImpl(inputChanges, outputsBackup)
        }

        buildMetricsReporterService.orNull?.also { it.addTask(path, this.javaClass, buildMetrics) }
    }

    private fun executeImpl(
        inputChanges: InputChanges,
        taskOutputsBackup: TaskOutputsBackup?
    ) {
//...
        callCompilerAsync(
            args,
            allKotlinSources,
            inputChanges,
            taskOutputsBackup
        )
    }

主要看執(zhí)行在executeImpl()中的callCompilerAsync()方法:

    override fun callCompilerAsync(
        args: K2JVMCompilerArguments,
        kotlinSources: Set<File>,
        inputChanges: InputChanges,
        taskOutputsBackup: TaskOutputsBackup?
    ) {
        validateKotlinAndJavaHasSameTargetCompatibility(args, kotlinSources)
//...
        val compilerRunner = compilerRunner.get()
//...
        compilerRunner.runJvmCompilerAsync(
            (kotlinSources + scriptSources).toList(),
            commonSourceSet.toList(),
            javaSources.files, // we need here only directories where Java sources are located
            javaPackagePrefix,
            args,
            environment,
            defaultKotlinJavaToolchain.get().providedJvm.get().javaHome,
            taskOutputsBackup
        )
    }

先獲取一個compilerRunner,它的實現(xiàn)如下:

    @get:Internal
    override val compilerRunner: Provider<GradleCompilerRunner> = objectFactory.propertyWithConvention(
        // From Gradle 6.6 better to replace flatMap with provider.zip()
        defaultKotlinJavaToolchain.flatMap { toolchain ->
            objectFactory.property(gradleCompileTaskProvider.map {
                GradleCompilerRunnerWithWorkers(
                    it,
                    toolchain.currentJvmJdkToolsJar.orNull,
                    normalizedKotlinDaemonJvmArguments.orNull,
                    metrics.get(),
                    compilerExecutionStrategy.get(),
                    workerExecutor
                )
            })
        }
    )

創(chuàng)建了一個GradleCompilerRunnerWithWorkers對象,再執(zhí)行compilerRunner.runJvmCompilerAsync()方法。繼續(xù)追蹤若干個調(diào)用,執(zhí)行了如下代碼:

    protected open fun runCompilerAsync(
        workArgs: GradleKotlinCompilerWorkArguments,
        taskOutputsBackup: TaskOutputsBackup?
    ): WorkQueue? {
        try {
            val kotlinCompilerRunnable = GradleKotlinCompilerWork(workArgs)
            kotlinCompilerRunnable.run()
        } catch (e: GradleException) {
//...
        }

        return null
    }

其中GradleKotlinCompilerWork是個Runnable,執(zhí)行了它的run()方法:

    override fun run() {
        try {
            val messageCollector = GradlePrintingMessageCollector(log, allWarningsAsErrors)
            val (exitCode, executionStrategy) = compileWithDaemonOrFallbackImpl(messageCollector)
//...
        } finally {
//...
        }
    }

繼續(xù)看的compileWithDaemonOrFallbackImpl()實現(xiàn):

    private fun compileWithDaemonOrFallbackImpl(messageCollector: MessageCollector): Pair<ExitCode, KotlinCompilerExecutionStrategy> {
//...
        if (compilerExecutionStrategy == KotlinCompilerExecutionStrategy.DAEMON) {
            val daemonExitCode = compileWithDaemon(messageCollector)
//...
        }

        val isGradleDaemonUsed = System.getProperty("org.gradle.daemon")?.let(String::toBoolean)
        return if (compilerExecutionStrategy == KotlinCompilerExecutionStrategy.IN_PROCESS || isGradleDaemonUsed == false) {
            compileInProcess(messageCollector) to KotlinCompilerExecutionStrategy.IN_PROCESS
        } else {
            compileOutOfProcess() to KotlinCompilerExecutionStrategy.OUT_OF_PROCESS
        }
    }

可以看出,kotlin編譯有三種策略,分別是

  • 守護進程編譯:Kotlin編譯的默認模式,只有這種模式才支持增量編譯,可以在多個Gradle daemon進程間共享
  • 進程內(nèi)編譯:Gradle daemon進程內(nèi)編譯
  • 進程外編譯:每次編譯都是在不同的進程
    我們來看Kotlin編譯的默認模式的實現(xiàn),其compileWithDaemon()方法最終執(zhí)行了編譯邏輯:
private fun compileWithDaemon(messageCollector: MessageCollector): ExitCode? {
//...
        val bufferingMessageCollector = GradleBufferingMessageCollector()
        val exitCode = try {
            val res = if (isIncremental) {
                incrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
            } else {
                nonIncrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
            }
//...
        } catch (e: Throwable) {
//...
        }
//...
        return exitCode
    }

到這里會執(zhí)行實現(xiàn)類org.jetbrains.kotlin.daemon.CompileServiceImplcompile 方法,這樣就終于調(diào)到了Kotlin編譯器內(nèi)部。經(jīng)過代碼追蹤,會先執(zhí)行compileImpl()方法,然后執(zhí)行到doCompile()方法:

 val compiler = when (targetPlatform) {
            CompileService.TargetPlatform.JVM -> K2JVMCompiler()
            CompileService.TargetPlatform.JS -> K2JSCompiler()
            CompileService.TargetPlatform.METADATA -> K2MetadataCompiler()
        } as CLICompiler<CommonCompilerArguments>

doCompile(sessionId, daemonReporter, tracer = null) { eventManger, profiler ->
                        val services = createServices(servicesFacade, eventManger, profiler)
                        compiler.exec(messageCollector, services, k2PlatformArgs)
                    }

我們繼續(xù)看compiler.exec()做了什么,跟進到K2JVMCompilerdoExecute()方法:
在實現(xiàn)了一系列的配置之后,我們找到了關(guān)鍵代碼:

KotlinToJVMBytecodeCompiler.compileModules(environment, buildFile, chunk)

繼續(xù)跟蹤compileModules()的關(guān)鍵代碼:

        val codegenInputs = ArrayList<CodegenFactory.CodegenInput>(chunk.size)

        for (module in chunk) {
//...
            codegenInputs += runLowerings(
                environment, moduleConfiguration, result, ktFiles, module, codegenFactory, backendInput, diagnosticsReporter
            )
        }

        val outputs = ArrayList<GenerationState>(chunk.size)

        for (input in codegenInputs) {
            outputs += runCodegen(input, input.state, codegenFactory, result.bindingContext, diagnosticsReporter, environment.configuration)
        }

        return writeOutputs(environment.project, projectConfiguration, chunk, outputs, mainClassFqName)

runLowerings()runCodeGen()看起來是我們想要的關(guān)鍵,其會執(zhí)行到繼續(xù)追蹤關(guān)鍵代碼:

return codegenFactory.invokeLowerings(state, backendInput)
            .also { performanceManager?.notifyIRLoweringFinished()
codegenFactory.invokeCodegen(codegenInput)

追蹤到CodegenFactory的相應(yīng)實現(xiàn)。又經(jīng)過若干跳轉(zhuǎn)后,執(zhí)行到MemberCodegen.genSimpleMember()。我們看看相應(yīng)的實現(xiàn):

public void genSimpleMember(@NotNull KtDeclaration declaration) {
        if (declaration instanceof KtNamedFunction) {
            try {
                functionCodegen.gen((KtNamedFunction) declaration);
            }
            catch (ProcessCanceledException | CompilationException e) {
                throw e;
            }
            catch (Exception e) {
                throw new CompilationException("Failed to generate function " + declaration.getName(), e, declaration);
            }
        }
        else if (declaration instanceof KtProperty) {
            try {
                propertyCodegen.gen((KtProperty) declaration);
            }
            catch (ProcessCanceledException | CompilationException e) {
                throw e;
            }
            catch (Exception e) {
                throw new CompilationException("Failed to generate property " + declaration.getName(), e, declaration);
            }
        }
//...
    }

根據(jù)declaration類型來決定用哪個Codegen去編譯,我們看方法的生成:functionCodegen.gen()的關(guān)鍵實現(xiàn):

generateMethod(JvmDeclarationOriginKt.OtherOrigin(function, functionDescriptor), functionDescriptor, strategy);

public void generateMethod(
            @NotNull JvmDeclarationOrigin origin,
            @NotNull FunctionDescriptor functionDescriptor,
            @NotNull MethodContext methodContext,
            @NotNull FunctionGenerationStrategy strategy
    ) {
//...
        Method asmMethod = jvmSignature.getAsmMethod();

//...
        MethodVisitor mv =
                strategy.wrapMethodVisitor(
                        newMethod(
                                origin,
                                flags,
                                asmMethod.getName(),
                                asmMethod.getDescriptor(),
                                strategy.skipGenericSignature() ? null : jvmSignature.getGenericsSignature(),
                                getThrownExceptions(functionDescriptor, typeMapper)
                        ),
                        flags, asmMethod.getName(),
                        asmMethod.getDescriptor()
                );

//..
            generateMethodBody(
                    origin, functionDescriptor, methodContext, strategy, mv, jvmSignature, staticInCompanionObject
            );
 //...
    }

這段代碼就是具體的編譯過程了,從命名來看,大概就是使用ASM框架去生成Java字節(jié)碼,其中mv是個MethodVisitor對象,在org.jetbrains.org.objectweb.asm包內(nèi)。Android的編譯過程我們就追蹤到這里。

  • 總結(jié)一下,Android aar的編譯過程是由KotlinMultiplatformPlugin發(fā)起kotlinTask,再由kotlinTask開啟KotlinCompile編譯任務(wù),并交給GradleCompilerRunnerWithWorkers執(zhí)行。在執(zhí)行過程中調(diào)到了Kotlin編譯器內(nèi)部org.jetbrains.kotlin.daemon.CompileServiceImpl的compile()方法,并交由CodegenFactory實現(xiàn),最終使用ASM框架去生成Java字節(jié)碼。

Android的編譯過程就看到這里,我們再來看看iOS的,由于本人是Android開發(fā),對于iOS的編譯過程只做個大概的說明。

5.2.iOS編譯過程

我們重新回到KotlinMultiplatformPluginsetupDefaultPresets()找到iOS相應(yīng)的配置方法:

val nativeTargetsWithSimulatorTests =
                setOf(IOS_X64, IOS_SIMULATOR_ARM64, WATCHOS_X86, WATCHOS_X64, WATCHOS_SIMULATOR_ARM64, TVOS_X64, TVOS_SIMULATOR_ARM64)

            HostManager().targets
                .forEach { (_, konanTarget) ->
                    val targetToAdd = when (konanTarget) {
                        in nativeTargetsWithHostTests ->
                            KotlinNativeTargetWithHostTestsPreset(konanTarget.presetName, project, konanTarget)
                        in nativeTargetsWithSimulatorTests ->
                            KotlinNativeTargetWithSimulatorTestsPreset(konanTarget.presetName, project, konanTarget)
                        else -> KotlinNativeTargetPreset(konanTarget.presetName, project, konanTarget)
                    }

                    add(targetToAdd)
                }

我們選取一個Preset看它的實現(xiàn),比如KotlinNativeTargetPreset,我們先看它父類AbstractKotlinNativeTargetPreset的初始化:

override fun createTarget(name: String): T {
        setupNativeCompiler()

        val result = instantiateTarget(name).apply {
            targetName = name
            disambiguationClassifier = name
            preset = this@AbstractKotlinNativeTargetPreset

            val compilationFactory =
                if (project.hasKpmModel) {
                    val kpmVariantClass = kpmNativeVariantClass(konanTarget) ?: error("Can't find the KPM variant class for $konanTarget")
                    KotlinMappedNativeCompilationFactory(this, kpmVariantClass)
                } else {
                    KotlinNativeCompilationFactory(this)
                }
            compilations = project.container(compilationFactory.itemClass, compilationFactory)
        }

        createTargetConfigurator().configureTarget(result)

//...

        return result
    }

其中setupNativeCompiler()方法的主要作用是創(chuàng)建一個NativeCompilerDownloader對象,下載一些必要的工具,然后創(chuàng)建一個KotlinCompilationFactory對象,編譯執(zhí)行的重點在:

createTargetConfigurator().configureTarget(result)

我們看一下createTargetConfigurator()創(chuàng)建了什么:

override fun createTargetConfigurator(): AbstractKotlinTargetConfigurator<KotlinNativeTarget> {
        val configurator = KotlinNativeTargetConfigurator<KotlinNativeTarget>()
        return if (project.hasKpmModel)
            KpmNativeTargetConfigurator(configurator)
        else configurator
    }

創(chuàng)建了一個KotlinNativeTargetConfigurator對象,接下來我們看看configureTarget()的實現(xiàn):

fun configureTarget(
        target: KotlinTargetType
    ) {
        configureCompilationDefaults(target)
        configureCompilations(target)
        defineConfigurationsForTarget(target)
        configureArchivesAndComponent(target)
        configureSourceSet(target)
        configureBuild(target)
        configurePlatformSpecificModel(target)
    }

做了很多配置,我們看看針對iOS單獨做了什么,繼續(xù)看configurePlatformSpecificModel()的實現(xiàn):

override fun configurePlatformSpecificModel(target: T) {
        configureBinaries(target)
        configureFrameworkExport(target)
        configureCInterops(target)

        if (target.konanTarget.family.isAppleFamily) {
            registerEmbedAndSignAppleFrameworkTasks(target)
        }

        if (PropertiesProvider(target.project).ignoreIncorrectNativeDependencies != true) {
            warnAboutIncorrectDependencies(target)
        }
    }

我們看一下configureBinaries(target)的主要實現(xiàn):

target.binaries.all {
            project.createLinkTask(it)
        }

創(chuàng)建了一個linkTask:

private fun Project.createLinkTask(binary: NativeBinary) {
//...
        if (binary is Framework) {
            createFrameworkArtifact(binary, result)
        }
    }

繼續(xù)追蹤createFrameworkArtifact()

private fun Project.createFrameworkArtifact(
        binary: Framework,
        linkTask: TaskProvider<KotlinNativeLink>
    ) {
        configurations.create(lowerCamelCaseName(binary.name, binary.target.name)) {
            it.isCanBeConsumed = true
            it.isCanBeResolved = false
            it.configureConfiguration(linkTask)
        }

        if (FatFrameworkTask.isSupportedTarget(binary.target)) {
            configureFatFramework()
        }
    }

先執(zhí)行了configureConfiguration(),后執(zhí)行了configureFatFramework(),其中linkTaskFatFrameworkTask,編譯任務(wù)最重都是由FatFrameworkTask來執(zhí)行的,其編譯過程在如下方法里:

@TaskAction
    protected fun createFatFramework() {
        val outFramework = fatFrameworkLayout

        outFramework.mkdirs()
        mergeBinaries(outFramework.binary)
        mergeHeaders(outFramework.header)
        createModuleFile(outFramework.moduleFile, fatFrameworkName)
        mergePlists(outFramework.infoPlist, fatFrameworkName)
        mergeDSYM()
    }

最終生成了可被iOS引用的framework。如果我們解壓framework文件,會發(fā)現(xiàn)這些merge方法與其framework的產(chǎn)物是一一對應(yīng)的。

6.總結(jié)

好了,對于KMM的介紹就到這里。KMM的特性意味著我們可以大膽的使用它,不用擔(dān)心性能問題,上架風(fēng)險等。雖然它還不夠成熟,支持性還不夠高,但考慮到它較低的學(xué)習(xí)成本(特別是對于Andorid開發(fā)來說),我們完全可以局部的使用它。對于前端開發(fā)同學(xué)來說,跨平臺是個總也繞不開的話題,學(xué)習(xí)不同的框架對于我們對App整體架構(gòu)的思考也起到積極的作用。

7.參考文獻

https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html
http://www.itdecent.cn/p/bd0cf9b2193c
https://blog.csdn.net/rain_butterfly/article/details/87941589

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容