Aengus 的技术小筑

Aengus | Blog

探索 Kotlin Multiplatform 全貌:原理、现状与未来

2024-11-29

本文主要是对 KMP 和 CMP 进行整体介绍,不会过多深入技术细节

介绍

Kotlin Multiplatform (以下称为 KMP)是 Jetbrains 推出的基于 Kotlin 语言的跨平台 SDK,其原理是先通过前端编译器将 Kotlin 代码编译为中间产物(intermediate representation, IR),然后再分别通过不同的后端编译器将 IR 编译为不同目标平台的二进制产物。

Kotlin Compiler

以“Hello,World”为例

fun main() {
  println("Hello, World!")
}

生成的 IR 如下:

MODULE_FRAGMENT name:<main>
  FILE fqName:<root> fileName:/var/folders/lt/j8a1bwfu92m5_qzvkp47y8xl0000rt/T/Kotlin-Compilation7335327567848552666/sources/main.kt
      FUN name:main visibility:public modality:FINAL <> () returnType:kotlin.Unit
          BLOCK_BODY
              CALL 'public final fun println (message: kotlin.Any?): kotlin.Unit [inline] declared in kotlin.io.ConsoleKt' type=kotlin.Unit origin=null
                  message: CONST String type=kotlin.String value="Hello, World!"

前端编译器目前主要有 K1 和 K2。K2 是新一代前端编译器,其在编译速度、语法分析等方面有很大提升。K2 在 2024.5 月份发布的 Kotlin 2.0.0 版本中已 Stable。

后端编译器主要有以下几种:

Kotlin/JVM:生成 Java 字节码以在 JVM 或安卓平台运行

Kotlin/Native:通过 LLVM 生成 iOS, macos, Linux 和 Windows (MinGW) 平台产物

Kotlin/JS:将 Kotlin 代码转换为 JavaScript 代码以在 Web 浏览器或 NodeJS 环境中运行

Kotlin/Wasm:编译为 WebAssembly 格式产物,目前还处在 alpha 阶段

历史

Kotlin History

优势

当说到跨平台时,通常的选择有以下几种:

  1. 借助语言的跨平台能力,将部分逻辑进行跨平台共享,如 C++、Rust,KMP 也属于此类;
  2. 使用跨平台库/SDK,实现逻辑与 UI 的共享,如 Flutter、React Native。

相较于其他跨端方案,KMP 在多个方面都有着其优势:

Kotlin 语言: 相较于 C++、Rust 等语言,Kotlin 的学习成本更低,Android 同学可以零成本上手,语言设计和 Swift 比较像,iOS 同学上手成本相对较低;相较于 Dart(Flutter) 语言,其语言设计更“先进”,语言提供的空安全、高阶函数、扩展属性/函数、Delegation、data class/sealed class 等特性以及配套完善的工具链对效率的提升很大;

接入成本低: 对于一个现有 Android 项目来讲,KMP 也可以以较低的成本接入,反观其他跨端方案基本上在编译以及打包上都有一定程度的适配工作量;

接入灵活,渐进式接入: 可以根据项目的实际需求灵活调整开发策略。既可以只选择部分逻辑跨端,UI 使用原生实现或其他框架,实现混合开发模式,也可以和 Compose 配合实现整体跨平台;

KMP arch

使用

共享代码放在共享模块中,异化逻辑使用 expect/actual关键字将不同实现分别放置在对应平台的模块内:

Kotlin Compile r

// commonMain
interface Platform {
    val name: String
}

expect fun getPlatform(): Platform

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

actual fun getPlatform(): Platform = AndroidPlatform()

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

actual fun getPlatform(): Platform = IOSPlatform()

可以使用平台类作为actual的实现,但必须是相同的类类型:

// commonMain
expect abstract class PlatformContext

// androidMain
actual typealias PlatformContext = android.content.Context

Kotlin 与 C/Objective-C 可以互相调用,Swift API 使用@objc导出为 OC 也可以被 Kotlin 调用,纯 Swift Module 暂时不支持被 Kotlin 调用。

业内实践

国内

当前国内绝大部分公司使用 KMP 主要是为了支持鸿蒙开发,

此处简单介绍一下腾讯 Kuikly 的设计:

Kuikly Design

整体思路是业务逻辑共享 + 自定义 UI 层协议 + Native UI 实现。业务代码通过 Kotlin 书写,生成对应平台的二进制产物;在 UI 层自己设计了一套声明式、响应式的框架,框架接口定义在公共层,实际则是由 Native View 实现,例如 View 的消失对应 Android 平台通过 remove View 实现。

为了尽可能实现 UI 一致性,将高阶组件的实现放在 common 层,Native 仅提供有限能力的原子组件:

Kuikly UI layer

海外

Forbes Mobile App Shifts To Kotlin Multiplatform

Mobile multiplatform development at McDonald’s

Netflix Android and iOS Studio Apps — now powered by Kotlin Multiplatform

以上均来自于 官网,里面还有很多其他 App 的实践,感兴趣的可以点进去看看。

Compose

Jetpack Compose 是 Android 官方推出的声明式、响应式 UI 框架。示例用法如下:

@Composable
fun JetpackCompose() {
    Card {
        var expanded by remember { mutableStateOf(false) }
        Column(Modifier.clickable { expanded = !expanded }) {
            Image(painterResource(R.drawable.jetpack_compose)
            AnimatedVisibility(expanded) {
                Text(
                    text = "Jetpack Compose",
                    style = MaterialTheme.typography.bodyLarge,
                )
            }
        }
    }
}

其在编译时通过 Compose Kotlin Compiler Plugin 将Composable函数在编译时转换为带有Composer参数的普通函数,这也是Composable只能在Composable函数中调用的原因(与suspend函数相似)。

Compose 架构分为四层,可以根据每层的功能进行灵活扩展:

Compose Arch

有一些利用 Compose 能力、比较有意思的第三方库:

Compose on the server side:使用 Compose runtime 层实现 server 端驱动的 Web 应用

redwood:跨平台 UI 库,可以通过自定义组件 Schema 以及对应的、在各平台上的实现,然后使用 Composable 函数的方式来调用这些组件

circuit:使用 Composable 函数进行状态管理

回到跨平台,Compose Multiplatform 是 Jetbrains 基于 Jetpack Compose 扩展的跨平台 UI SDK,目前已支持绝大部分主流平台:

Compose Arch

问题与挑战

性能

由于 Kotlin 本来就是 Android 开发的首选语言,所以在 Android 平台可以认为是无性能损耗,并且 Compose 1.7 版本其性能在某些方面甚至超过了原生 View。而在其他平台,其性能损耗主要来自原生与 Kotlin 之间的调用。

包体积

在非 Android 平台上,由于 Kotlin 标准库的存在,包体积会有一定的劣化;如果使用 Compose 跨平台,由于 Skia 引擎的存在,包体积劣化更明显。以使用 Swift UI、新建的空项目为例,iOS 产物(.xarchive 格式,打包为 ipa 差距将会减小)大小对比如下:

产物大小
Native1.2M
KMP + Native UI2.4M
KMP + Compose38.5M

研发体验

Common 层无法直接依赖平台库导致开发效率折损。 Android 中很多能力如获取文案、分辨率获取等都要依赖 Activity 或者 Context,但在 common 层无法直接访问到平台特有的类,只能通过定义接口来实现业务逻辑的复用,“多余的”抽象带来了开发效率的下降,涉及到 Activity 重建场景下的处理会更麻烦。长期来看,可能出现异化代码增量比共享代码增量更快的问题。

Native 调用 Kotlin 能力限制与多 Module 依赖限制。 详见 Android App 迁移 KMM 实践#Swift 调用及限制

三方库生态不足。 三方库缺失应该是所有新技术都面临的问题,尤其是涉及到一些比较细化的需求,库缺失是一个比较大的痛点。不过近来第三方库增长比较快,相信后面这个问题会越来越小。

未来

Kotlin Multiplatform Development Roadmap for 2025

  • Compose Multiplatform: iOS Stable,提升 Jetpack Compose 功能对等性、iOS 渲染性能、核心组件功能完整性等,优化 Web 和 Desktop target,并完善文档
  • 独立 KMP IDE: 基于 JetBrains Fleet 推出专门针对 Kotlin Multiplatform 开发的 IDE
  • Kotlin-to-Swift 导出: 2025 年目标是发布第一个 Public 版本,提供类似现有 Objective-C 导出体验
  • 库生态: 通过改进 klib 格式、实现与 JVM 相同的代码内联行为、提供兼容性工具、改善发布流程和文档过程等措施,确保 Kotlin Multiplatform 库的向后兼容性
  • 跨平台库搜索: 更轻松地找到满足需求的库
  • Amper 聚焦于使 Amper 完全适用于 Android 和 iOS 多平台移动应用开发,包括运行和测试应用、签名和发布应用以及提供众多 IDE 功能,还探索服务器端开发场景
  • Gradle 增强: 支持在项目级别声明 Kotlin Multiplatform 依赖项、改进 Kotlin/Native 工具链与 Gradle 的集成、实现多平台库的下一代分发格式等

从最近 KMP/CMP 更新发布频率来看,官方在 KMP 的投入比较高,各种第三方库更新也比较频繁,包括 Android 官方的 Jetpack 库如 DataStore/Room/Navigation/... 都支持了跨平台,Jetbrains 也将 Lifecycle/ViewModel 库做了适配,无论是新项目还是将现有项目往跨平台迁移,都值得一试。

推荐阅读