KMM初探与编译过程详解

news2024/11/15 5:39:30

7560bca8989190cb44040f825f4c64a4.gif

本文字数:22817

预计阅读时间:58分钟

简介

KMM, 即Kotlin Multiplatform Mobile,是由Kotlin发布的移动端跨平台框架。相比于其他跨平台框架,KMM是原生UI+逻辑共享的理念,共享重复逻辑性的工作来提升开发效率的同时,保持原生执行效率与UI特性。所以KMM并不会替代AndroidiOS的原生开发, 而是提倡将共有的逻辑部分抽出,由KMM封装成Android(Kotlin/JVM)aariOS(Kotlin/Native)framework,再提供给View层进行调用,从而节约一部分的工作量。 我们先来比较几种当前流行的跨平台框架:

3c80cfdb9228cfa38140b6ba4637c734.jpeg

主要说两点:

  • 由于KMM的本质就是原生App,跨平台共享的内容都是在编译期进行的处理,所以在性能方面可以说是不受影响。其他的跨平台方案,其性能多多少少都会受到影响且会增加包体积。

  • Flutter为代表的自带渲染引擎实现UI框架在开发效率上是更高的。而KMM主要实现的是共享逻辑,UI层的实现还是建议平台各自去处理,所以开发效率上来说,KMM优于原生开发,但不如Flutter。不过由于Android的官方语言就是Kotlin,对于Android开发来说,KMM的加持更像是一种赠送能力,几乎可以无成本的进行KMM开发。

另外,去年十月初Android 官方宣布 Jetpack开始要支持KMM了,意味着KMM已经得到了官方的支持。目前Collections和 DataStore 已经可以通过依赖 -dev01 版本在多平台上使用,加上KMM已经到了Beta的阶段,到了我们可以进行大胆尝试的时候了。因此本文将从准备工作,结构介绍,Demo示例和编译过程详解来对KMM框架进行说明。

准备工作

安装必要的工具和插件

  • 1.安装或更新AndroidStudio和Xcode到最新版

  • 2.JDK

  • 3.Kotlin Multiplatform Mobile plugin:AndroidStudio下载KMM插件,如图所示:

85c11d6ac9bb0906fedc1050ff06af15.jpeg

image.png
  • 4.Kotlin plugin:更新Kolin Plugin到最新版本,如图所示:

0f2d80e7c6a631401dac2cc9bd2a9533.jpeg

检查环境配置

  • 1.打开Terminal使用HomeBrew安装以下工具:

brew install kdoctor

  • 2.执行kdoctor:

kdoctor

如果你的环境配置有问题,那么将会输出相应的信息,并以X作为标记,大家可根据提示进行修改。Tips:以上步骤可能需要科学上网。

KMM结构说明

在尝试KMM之前,我们先来了解一下KMM的基本结构。我们建个默认的KMM工程,看一下它的结构:99f958f61d5fece214a181e5ab800ced.jpeg默认会生成androidAppsharediosApp这三个子工程。其中androidAppiosApp为Android和iOS这两个平台的工程模块,shared为共享逻辑模块,供androidAppiosApp调用。我们打开根目录的settings.gradle.kts

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

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

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

会发现主项目只includeandroidAppshared这两个子项目,因为这两个项目是Gradle项目,那么iOS的项目如何引用呢?我们摘抄一段官网的原文:

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有自己的编译系统,因此iOS项目并不依靠Gradle去和共享工程建立联系,而是依靠将共享工程打包成frameworkiOS项目使用。我们可以看一下shared模块编译后的产物,如下图所示:

2735e102526f7c80191d3d6bd3e41397.jpeg

可以很清晰的看到生成的frameworkaar文件。我们再来看看shared模块都包含了什么:

a4b822480215f34355ec93d8a4c191e5.jpeg

  • 其中commonMain为公共模块,该模块的代码与平台无关,是通过 expected关键字对一些api的声明(声明的实现在platform module中)。

  • androidMainiosMain分别为AndroidiOS这两个平台,通过actual关键字在平台模块进行具体的实现。

我们继续看看shared模块的gradle文件都做了什么:

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

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

这是将这个KMM模块编译成Android aariOS framework的声明。使用了Gradle编译系统和KMM插件来进行实现。其中:

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进行编译的,相应的配置是用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")
            }
        }
//...
    }
}

在上一节我们提到的commonMainandroidMainiosMain就是sourceSets,其中commonMain是共享的逻辑,而androidMainiosMain是对AndroidiOS共享逻辑的相应实现。每一个sourceSet都可以有自己单独的dependencies,如上面的代码。支持分别引入implementation来实现各自的逻辑。另外Kotlin标准库会被自动加到相应的sourceSet中,无需重复引入。比方说上面的kotlinx-coroutines-core。之后,AndroidiOS分别使用各自常规方式引入/调用aarframework即可。经过以上的说明,我们大概了解了KMM,项目的架构,抄一张官网的图进行总结:

4ed3bf3f81ae6a2450d68c4275c4613a.jpeg

Demo示例 

环境配置好了,我们就可以写一个简单的demo了。打开Android StudioFile->New->New Project,选择Kotlin Multiplatform App

a36a3b85e02db02549b511c1625e3abf.jpeg

点击Next

c9acb5f0ab9a4dcd42bb876b0f806483.jpeg

继续点击Next

ee97bec1daf2d5b44f2cfde2dbe0270c.jpeg

配置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不需要三方工具,且有较少的安装事项。对于更复杂的工程来说,可能需要使用CocoaPods dependency manage去管理libs。对于我们的Demo工程来说,先使用Regular framework来进行说明。在上个章节我们对于KMM工程的结构进行了说明,我们进入shared模块,看commonMain文件夹下Greeting的实现:

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

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

greeting()方法调用platform.namePlatform的实现如下:

interface Platform {
    val name: String
}

expect fun getPlatform(): Platform

Platform是个接口,使用expect关键字来声明getPlatform(),再由AndroidiOS通过使用actual关键字分别实现:

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()

最后我们看一下是如何调用的:

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,同意一些协议,然后回到Android Studioconfigurations list中选择iosApp,直接运行即可。

效果如下:

5652d8d748a315e30137e92f80211f9d.jpeg

编译过程详解 

接下来我们来看一下shared模块是如何工作的。先关注一下shared模块的gradle文件:

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

加载了multiplatform插件,并把此module作为lib输出。

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

重点来了,这个是编译双端lib的关键。我们先看看android()是怎么编译Androidaar的:

Android编译过程

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

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

我们看到android()是创建了个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()方法的实现:

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

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

截取重要的部分,看setupDefaultPresets(project)的实现:

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)
                }
        }
    }

这个方法创建并添加了各个平台的TargetPreset,看上去是用来配置各个平台的编译详情的。我们先看看Android平台KotlinAndroidTargetPreset的是如何创建的:

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
    }

创建了一个KotlinAndroidTarget对象,并执行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()的实现:

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

做了一些初始配置,并为tasksProvider注册了KotlinJVMTask。看postprocessVariant()的实现:

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)
    }

创建了javaTaskkotlinTask,然后看wireKotlinTasks()的实现:

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 })
        }
//...
    }

从方法命名我们可得知,它的目的是执行KotlinTasks。我们看见kotlinTask是个TaskProvider对象,实际的操作在KotlinCompile里。KotlinCompile是个Task,其execute实现在父类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
        )
    }

主要看执行在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,它的实现如下:

@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
                )
            })
        }
    )

创建了一个GradleCompilerRunnerWithWorkers对象,再执行compilerRunner.runJvmCompilerAsync()方法。继续追踪若干个调用,执行了如下代码:

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

        return null
    }

其中GradleKotlinCompilerWork是个Runnable,执行了它的run()方法:

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

继续看的compileWithDaemonOrFallbackImpl()实现:

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进程间共享

  • 进程内编译:Gradle daemon进程内编译

  • 进程外编译:每次编译都是在不同的进程 我们来看Kotlin编译的默认模式的实现,其compileWithDaemon()方法最终执行了编译逻辑:

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
    }

到这里会执行实现类org.jetbrains.kotlin.daemon.CompileServiceImpl 的 compile 方法,这样就终于调到了Kotlin编译器内部。经过代码追踪,会先执行compileImpl()方法,然后执行到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)
                    }

我们继续看 compiler.exec()做了什么,跟进到K2JVMCompilerdoExecute()方法:在实现了一系列的配置之后,我们找到了关键代码:

KotlinToJVMBytecodeCompiler.compileModules(environment, buildFile, chunk)

继续跟踪compileModules()的关键代码:

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()看起来是我们想要的关键,其会执行到继续追踪关键代码:

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

追踪到CodegenFactory的相应实现。又经过若干跳转后,执行到MemberCodegen.genSimpleMember()。我们看看相应的实现:

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);
            }
        }
//...
    }

根据declaration类型来决定用哪个Codegen去编译,我们看方法的生成: functionCodegen.gen()的关键实现:

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字节码,其中mv是个MethodVisitor对象,在org.jetbrains.org.objectweb.asm包内。Android的编译过程我们就追踪到这里。

  • 总结一下,Android aar的编译过程是由KotlinMultiplatformPlugin发起kotlinTask,再由kotlinTask开启KotlinCompile编译任务,并交给GradleCompilerRunnerWithWorkers执行。在执行过程中调到了Kotlin编译器内部org.jetbrains.kotlin.daemon.CompileServiceImpl的compile()方法,并交由CodegenFactory实现,最终使用ASM框架去生成Java字节码。

Android的编译过程就看到这里,我们再来看看iOS的,由于本人是Android开发,对于iOS的编译过程只做个大概的说明。

iOS编译过程 

我们重新回到KotlinMultiplatformPluginsetupDefaultPresets()找到iOS相应的配置方法:

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看它的实现,比如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()方法的主要作用是创建一个NativeCompilerDownloader对象,下载一些必要的工具,然后创建一个KotlinCompilationFactory对象,编译执行的重点在:

createTargetConfigurator().configureTarget(result)

我们看一下createTargetConfigurator()创建了什么:

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

创建了一个KotlinNativeTargetConfigurator对象,接下来我们看看configureTarget()的实现:

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

做了很多配置,我们看看针对iOS单独做了什么,继续看configurePlatformSpecificModel()的实现:

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)的主要实现:

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

创建了一个linkTask

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

继续追踪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()
        }
    }

先执行了configureConfiguration(),后执行了configureFatFramework(),其中linkTask FatFrameworkTask,编译任务最重都是由FatFrameworkTask来执行的,其编译过程在如下方法里:

@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文件,会发现这些merge方法与其framework的产物是一一对应的。

总结

好了,对于KMM的介绍就到这里。KMM的特性意味着我们可以大胆的使用它,不用担心性能问题,上架风险等。虽然它还不够成熟,支持性还不够高,但考虑到它较低的学习成本(特别是对于Andorid开发来说),我们完全可以局部的使用它。对于前端开发同学来说,跨平台是个总也绕不开的话题,学习不同的框架对于我们对App整体架构的思考也起到积极的作用。

参考文献

  • https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html

  • https://www.jianshu.com/p/bd0cf9b2193c

  • https://blog.csdn.net/rain_butterfly/article/details/87941589

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/443081.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Maven配置国内源以及jar下载失败处理详解

目录 1&#xff0c;配置Idea的Maven xml文件不存在&#xff1a; xml文件存在&#xff1a; 2&#xff0c;重新下载jar包 3&#xff0c;注意事项总结 1&#xff0c;配置Idea的Maven 需要配置的项目有两个&#xff0c;一个是当前项目&#xff0c;一个是新项目&#xff1a; 打…

抖音账号矩阵搭建管理获客系统

抖音矩阵号管理系统是一款企业矩阵运营管理工具&#xff0c;能够有效地帮助企业管理多个矩阵账号&#xff0c;并实现批量管理。在短视频矩阵系统中&#xff0c;自动获客工具和智能AI的帮助下&#xff0c;一个人也能轻松地管理多个账号。 一、矩阵账号管理&#xff1a; 首先&a…

leetCode算法第三天

继续练习leetcode中关于字符串的算法题&#xff0c;越练越觉得自己编码思想还很欠缺&#xff0c;继续努力。 文章目录 有效的括号括号生成串联所有单词的子串最长有效括号 有效的括号 leetcode链接&#xff1a;https://leetcode.cn/problems/valid-parentheses/ 解题思路&…

SPI协议

SPI数据接口 SPI&#xff08;Serial Peripheral Interface&#xff09;串行外设接口的简称&#xff0c;它是一种同步全双工通信协议。有 3根或者 4根数据线组成&#xff0c;包括 CLK、SOMI、SIMO、STE&#xff1a; CLK为时钟线&#xff0c;由主机控制输出。 SOMI…

国产数字温度传感芯片M117 Pin to Pin替代PT100和PT1000

高精度数字温度传感芯片 - M117&#xff0c;可Pin to Pin替代PT100/PT1000&#xff0c;且具功能差异化优势&#xff0c;支持行业应用的定制化需求。高测温精度0.1℃&#xff0c;用户无需进行校准。芯片感温原理基于CMOS半导体PN节温度与带隙电压的特性关系&#xff0c;经过小信…

电脑开机进不了系统卡在加载界面怎么办?

电脑开机进不了系统卡在加载界面怎么办&#xff1f;有用户电脑弹出需要进行系统更新&#xff0c;不小心点到了系统更新的选项。因为自己不想进行系统更新&#xff0c;所以马上将电脑关机了。但是关机之后却发现系统一直卡在开机的界面中&#xff0c;无法进入桌面中了。那么这个…

如何在Anaconda下安装pytorch(conda安装和pip安装)

前言 文字说明 本文中标红的&#xff0c;代表的是我认为比较重要的。 版本说明 python环境配置&#xff1a;jupyter的base环境下的python是3.10版本。CUDA配置是&#xff1a;CUDA11.6。目前pytorch官网提示支持的版本是3.7-3.9 本文主要用来记录自己在安装pytorch中…

乙肝80%以上由妈妈传给孩子 5岁以下治愈率超六成

中国是乙肝大国。目前&#xff0c;乙肝病毒感染人数仍超过7000万。通过医务人员多年的努力&#xff0c;母婴传播感染率明显下降。到目前为止&#xff0c;已降至0.3%左右。每年仍有5万名儿童感染乙肝病毒。目前&#xff0c;儿童慢性乙肝仍在180万左右&#xff0c;绝对数仍是世界…

Node【模块系统】

文章目录 &#x1f31f;前言&#x1f31f;Nodejs模块系统&#x1f31f;为什么需要模块化&#x1f31f;什么是Nodejs模块&#x1f31f;Nodejs模块分类&#x1f31f;文件模块的分类&#x1f31f;调用内置模块&#x1f31f;调用文件模块 &#x1f31f;Nodejs模块使用&#x1f31f;…

2023年网络安全的发展趋势是怎样的?

数据安全越来越重要。 我国《数据安全法》提出“建立健全数据安全治理体系”&#xff0c;各地区部门均在探索和简历数据分类分级、重要数据识别与重点保护制度。 数据安全治理不仅是一系列技术应用或产品&#xff0c;更是包括组织构建、规范制定、技术支撑等要素共同完成数据…

PACS/RIS影像管理系统源码,支持图像后处理与重建

PACS/RIS影像管理系统源码&#xff0c;功能强大&#xff0c;文档齐全&#xff0c;有演示。 文末获取联系&#xff01; 系统特点&#xff1a; 符合国内医院影像中心/放射科的典型工作管理流程。 开放式体系结构&#xff0c;完全符合DICOM3.0标准&#xff0c;提供HL7标准接口&a…

MyBatis(十四)MyBatis的逆向工程

前言、 所谓的逆向工程是&#xff1a;根据数据库表逆向生成Java的pojo类&#xff0c;SqlMapper.xml文件&#xff0c;以及Mapper接口类等。 要完成这个工作&#xff0c;需要借助别人写好的逆向工程插件。 思考&#xff1a;使用这个插件的话&#xff0c;需要给这个插件配置哪些…

2023年淮阴工学院五年一贯制专转本退役士兵大学语文考试大纲

2023年淮阴工学院五年一贯制专转本退役士兵大学语文考试大纲 一、考试目标 淮阴工学院五年一贯制高职专转本入学考试秘书学专业《大学语文》考试是我校为招收五年一贯制高职专转本学生设置的具有选拔性质的考试科目。其目的是科学、公平、有效地测试考生是否具备攻读秘书学本…

【论文总结】V-Shuttle:可扩展和语义感知的 Hypervisor 虚拟设备模糊测试

介绍 这是来自2021 CCS的一篇论文&#xff0c;作者有GaoningPan, Xingwei Lin, Xuhong Zhang, Yongkang Jia, Shouling Ji, Chunming Wu, Xinlei Ying, Jiashui Wang, Yanjun Wu。该论文提出V-shuttle的新框架来执行管控程序的模糊测试&#xff0c;该框架执行可扩展和语义感知…

LDR6328 PD诱骗(取电)芯片概述,支持定制化取电

PD充电需要在供电端&#xff08;充电器&#xff09;和受电端&#xff08;产品上&#xff09;都要有协议通信&#xff0c;一般充电器内有PD供电协议芯片&#xff0c;产品上有PD协议受电芯片&#xff0c;两者连接后会进行通信握手&#xff0c;连接成功后充电器才会输出需要的电压…

银行数字化转型导师坚鹏:商业银行对公业务数字化风控

商业银行对公业务数字化风控 课程背景&#xff1a; 数字化背景下&#xff0c;很多银行存在以下问题&#xff1a; 不清楚商业银行数字化风控发展现状&#xff1f; 不清楚对公业务数字化风控工作如何开展&#xff1f; 不知道零售业务数字化风控工作如何开展&#xff1f; …

探索五大机器学习技术及其应用

没有一种机器学习算法可以解决所有类型的机器学习问题。机器学习任务可能千差万别&#xff0c;算法的选择将取决于数据的大小、维数和稀疏性等因素。目标变量、数据的质量以及特征内部以及特征与目标变量之间存在的相互作用和统计关系。 在本文中&#xff0c;我将提供机器学习…

【NPM】npm上传包

必须使用npm镜像&#xff0c;不能使用淘宝镜像 查看当前的镜像 npm config get registry切换镜像为npm &#xff08;不能使用淘宝镜像&#xff09; npm config set registry https://registry.npmjs.org在要发布的包文件目录执行 发布为公共包 npm publish --access publi…

【基于准交叉双边滤波:医学图像】

Medical image fusion based on quasi-cross bilateral filtering &#xff08;基于准交叉双边滤波的医学图像融合&#xff09; 图像融合技术是利用特定的算法从多幅图像中提取并融合特征&#xff0c;然后将它们组合成一幅图像的方法。然而&#xff0c;目前的图像融合方法大多…

学生管理系统【GUI/Swing+MySQL】(Java课设)

系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设&#xff01;&#xff01;&#xff01; 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址&#xff1a;​​​​​​​https://download.csdn.net/download/qq_50954361/87700422 …