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層進(jìn)行調(diào)用,從而節(jié)約一部分的工作量逢享。
我們先來比較幾種當(dāng)前流行的跨平臺框架:


截屏2022-11-18 15.07.18.png

主要說兩點:

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

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

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ù)提示進(jìn)行修改。
Tips:以上步驟可能需要科學(xué)上網(wǎng)茁裙。

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

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

截屏2022-11-15 15.37.29.png

默認(rèn)會生成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)鍵字在平臺模塊進(jì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插件來進(jìn)行實現(xiàn)链方。其中:

kotlin("multiplatform")

意味著引入KMM 插件持痰。

id("com.android.library")

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

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

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

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)的圖進(jìn)行總結(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來進(jìn)行說明铃剔。
在上個章節(jié)我們對于KMM工程的結(jié)構(gòu)進(jìn)行了說明,我們進(jìn)入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()
    }
}

我們運(yùn)行一下普气。

  • Android的話,在Android Studio上的configurations list中選擇androidApp佃延,直接連接真機(jī)或開啟模擬器现诀,點擊Run->Run 'androidApp'即可。
  • iOS的話履肃,如果是第一次運(yùn)行仔沿,需要打開Xcode,同意一些協(xié)議尺棋,然后回到Android Studio封锉,configurations list中選擇iosApp,直接運(yùn)行即可膘螟。

效果如下:


截屏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。看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編譯有三種策略缕坎,分別是

  • 守護(hù)進(jìn)程編譯:Kotlin編譯的默認(rèn)模式怖侦,只有這種模式才支持增量編譯,可以在多個Gradle daemon進(jìn)程間共享
  • 進(jìn)程內(nèi)編譯:Gradle daemon進(jìn)程內(nèi)編譯
  • 進(jìn)程外編譯:每次編譯都是在不同的進(jìn)程
    我們來看Kotlin編譯的默認(rèn)模式的實現(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()做了什么,跟進(jìn)到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.參考文獻(xiàn)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市溉浙,隨后出現(xiàn)的幾起案子奇颠,更是在濱河造成了極大的恐慌,老刑警劉巖放航,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈拒,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)荆几,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門吓妆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吨铸,你說我怎么就攤上這事行拢。” “怎么了诞吱?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵舟奠,是天一觀的道長。 經(jīng)常有香客問我房维,道長沼瘫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任咙俩,我火速辦了婚禮耿戚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阿趁。我一直安慰自己膜蛔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布脖阵。 她就那樣靜靜地躺著皂股,像睡著了一般。 火紅的嫁衣襯著肌膚如雪命黔。 梳的紋絲不亂的頭發(fā)上屑墨,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音纷铣,去河邊找鬼卵史。 笑死,一個胖子當(dāng)著我的面吹牛搜立,可吹牛的內(nèi)容都是我干的以躯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼啄踊,長吁一口氣:“原來是場噩夢啊……” “哼忧设!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起颠通,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤址晕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后顿锰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谨垃,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡启搂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了刘陶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莹妒。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡匈睁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出梳毙,到底是詐尸還是另有隱情蜓席,我是刑警寧澤欢策,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布哼丈,位于F島的核電站臣樱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏再膳。R本人自食惡果不足惜挺勿,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饵史。 院中可真熱鬧,春花似錦胜榔、人聲如沸胳喷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吭露。三九已至,卻和暖如春尊惰,著一層夾襖步出監(jiān)牢的瞬間讲竿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工弄屡, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留题禀,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓膀捷,卻偏偏與公主長得像迈嘹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子全庸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容