Kotlin Multiplatform 原理深入分析

360影视 动漫周边 2025-09-15 18:35 1

摘要:KMP(Kotlin multiplatform)是 Kotlin 语言的一项重要特性,允许将 kotlin 代码运行在不同平台上,通过『一码多端』的方式来节省成本。

什么是 KMP

KMP(Kotlin multiplatform)是 Kotlin 语言的一项重要特性,允许将 kotlin 代码运行在不同平台上,通过『一码多端』的方式来节省成本。

而与诸如 java / React 这类跨端方案不同,KMP 没有采用所谓的虚拟机的思路,而是选择直接将 kotlin 源码编译成目标平台代码运行的方案。

KMP 的优势和限制

KMP 的优势:

相较传统的跨平台框架而言,由于 Kotlin 会将代码编译成目标平台原生代码执行(可以简单理解为将 Kotlin 源码翻译成 java/c++/js 代码),其最大的优势在于进行 FFI(跨语言调用)时几乎没有性能折损,并且执行性能接近于原生系统。

KMP 的限制:

由于早期的 kotlin 是基于 java / android 平台,这些 kotlin 二/三方库在设计时候也不可能考虑过跨平台。考虑到这些情况,kotlin 在编译时使用了 Target Platform概念,即 kotlin 每个类 / 方法 都是有对应平台的,早期的的 java / android 二三方库只属于 jvm 平台,意味只能在 java / android 平台调用,在其他平台上调用会编译报错。

而对于系统接口,在 KMP 下也是有对应平台限制的,一个简单的判定方法如下:

所有 kotlin.*、kotlinx.*包名的接口,都是跨平台的。

所有 java.*、sun.* 包名的接口,只能在 jvm 平台使用。

所有 android.*包名的接口,只能在 android 平台使用。

androidx.*比较特殊,部分库可以(比如 Room),部分不行,需要自行查看文档判断。

因此,如果想要将 Android 代码通过 KMP 直接编译成其他平台产物,那基本上是不可能直接成功的。如果没有提前设计隔离层的话,工程中二、三方依赖,以及源码中几乎不可避免的含有 Android / jvm 的平台接口,你很可能需要进行大量的抽象改造才可以完成。

KMP 实现原理

跨平台概述

前面提到,KMP 核心思路,是直接将 kotlin 源码编译成目标平台代码运行。而实现这一能力的关键就是 Kotlin 编译器,其核心职责就是将源码翻译成目标平台代码。

在实现上,kotlin 编译器使用了前后端分离的思路。

简单来说,前端负责语法解析&代码分析、后端负责将前端产物翻译成目标平台代码,二者职责清晰。未来如果如果需要支持一个新平台,添加一个新后端即可。

至于 Optimizer,由于不同目标平台的优化方式不同,在 kotlin 编译器中被放在了后端中。

Kotlin Native 编译器

由于 Kotlin Jvm 大家相对比较熟悉,而 Kotlin JS 笔者还没有看,因此本文只着重介绍 Kotlin Native 的相关分析

编译流程

Kotlin Native 编译入口通常为 Gradle Task 或者命令行(konanc),二者最终执行代码是共通的,最终会根据根据产物类型不同执行不同逻辑。

产物分为四类:

Klib:Kotlin Native Library,可以简单理解为 Kotlin Native 版本的 jar / aar,只保存了 kotlin ir 信息。

ObjCFramework:给 iOS 使用的 .framework.

Binary:缓存 / 可执行文件。

CLibrary:动态库 / 静态库。

produce(Klib)

Compile 作用是将 kt 编译成 Klib(可以类比为 aar)。

Klib 解开后结构如下:

所有 KN 模块在编译阶段都会先编译成 Klib,在 link 阶段才会调用 c++ 工具链处理。

produce(Binary/CLibrary/ObjCFramework)

这三个基本流程都差不多,都是将多个 Klib 聚合编译成一个二进制库(类似于 C 的 link、或者 android 的打 apk),区别在于产物不同。核心为编译器后端处理,用于将 kotlin ir 转换为目标平台的二进制库,核心流程如下:

各步骤说明:

1. Add entry points:如果编译可执行文件,就加一个入口文件的 ir file,比较简单。

2. Lowering module && dependencies:将所有依赖库合并,并针对合并后的每个 ir 文件(包括依赖的库的 ir)执行 Lowerings(对 Ir 进行前置优化,比如内联,语法糖处理),每个 lowering 文件需要执行 51 步,每一步都可以在 NativeLoweringPhases.kt 中找到对应的定义。

3. Run after lowering:即真正的 Native 编译流程,主要通过 llvm 将 kotlin IR 翻译为二进制产物,主要步骤:

CodeGen:将 kotlin ir 『翻译』成 llvm IR,这部分主要通过调用 llvm 的 c 函数实现

Generate Export Api + Compile Export Api:生成一个对外 api 的 c++ 接口文件并编译,用于暴露接口给外部调用。

Post Processing :在和底层依赖库(Runtime)的 bit code 链接前,做一些优化工作,比如去除无用代码。

Write bitCode:将所有 bitcode 链接完毕后,生成 out.bc

Compile and link:

调用 clang 将 .bc 编译成 .o,这里会根据 debug / release 添加不同编译参数。

调用 lld 将 .o 文件 link 成目标平台汇编代码

IR 转换

假设有如下源码:

classHelloWorld{ funhelloFun1(a: Int, b: Int): Int { return a + b }}

其编译后的 llvm ir 长这样:

除开一些流转指令、调试指令外、其翻译回 C / C++ 代码大概是这样。

// 没错这个函数名就是这么长int"kfun:com.demo.kmp.HelloWorld#helloFun1(kotlin.Int;kotlin.Int){}kotlin.Int"(*struct.ObjHeader this,int a,int b) { return a + b;}

可以看出和用 C / C++ 写的代码基本上差不多,所以执行效率是非常高的(相当于写 C / C++ 代码去运行)。

其主要的『翻译』逻辑如下:

kotlin 基础类型会『翻译』为对应的 C 的基本数据类型,如: int / float / double / short / long / double。

Kotlin 类会『翻译』成 llvm typeInfo 形式,用来记录类名等信息。

Kotlin 对象会『翻译』成 ObjHeader + 一段内存空间形式,前者用于记录 typeinfo,后者用来存放所有的类字段。

Kotlin 函数会『翻译』成 C 函数,差别在于会多一个 ObjHeader* 参数,用作 $this 指针。

Kotlin 属性会『翻译』成 Get/Set 函数,这个跟 java 是一致的。

Kotlin 运算符会『翻译』成对应的 operator 函数(举例来说,加号(+)会翻译成 add 函数),一些类型(比如基础类型)会进一步通过内联翻译成 C 的运算符。

其余类型则不再赘述,有兴趣可以自行参考源码(位于ir2bitcode.kt)实现。

Kotlin Native 运行时

为了实现内存的自动回收,在 Kotlin Native 平台上,会打包一套 Kotlin Runtime到最终产物中,包含异常处理、线程管理、内存管理等常规能力。

运行时包括如下几个部分,创建线程或者已存在的线程都可以 initRuntime

SetKonanTerminateHandler 为线程设置异常处理Handler,这样可以捕获kotlin excepiton

globalData 初始化全局变量

theaddata 初始化线程内存分配器

workInit 初始化线程消息队列,用于执行协程

和 android 相比,kmp 运行时不支持 synchronized 关键字,可以使用 atomicFu 来解决。

内存管理

Kotlin Native 有 3 种内存分配器:

custom:kotlin 自己开发的内存分配器,也是默认的内存分配器

std:标准库内存分配器,在鸿蒙上是 jemalloc

mimalloc:微软开源的 native 分配器

目前 std/mimalloc 在最新版本已经去掉了,kmp 未来会持续优化 custom 内存分配器

custom 内存分配器是 kotlin 自己实现的内存分配器,包括几个部分

Safealloc mmap 虚拟内存,每次大小256k,分配后检查是否需要触发 alloc gc

CreateObject 分配对象,每个对象额外增加16字节内存,包括 objectData/objectHeader

CreateObject 分配对象时,如果类(typeInfo)加了 TF_HAS_FINALIZER 标记,会通过 extraObject 增加对象弱引用,gc 后调用对象 finialize 方法,objectHeader 指向 extraObject

CreateArray 分配 array,每个 array 额外增加24字节内存,包括 objectData/ArrayAHeader,ArrayHeader 12字节按照8字节对齐到16字节

和 android 相比,有 3 点不同:

Kotlin Native 只支持 Weakreference,不支持 SoftReference

Kotlin Native 对象分配支持逃逸分析,除了在堆上分配,还可以在编译时通过静态代码分析决定哪些变量在栈上分配

Kotlin Native 把 Array 类型单独拿出来了,Android 认为所有类型都是 Object

基础类型

基础类型包括 Byte/Short/Int/Float/String 等,和 android 一致

对象类型

class 包括几部分

instanceSize_:对象大小,如果是 array,instanceSize_ 为每个元素大小

superType: 父类

objOffsets:成员变量 offset 数组,根据 offset 查找成员变量

objOffsetCount_:成员变量数量

interfaceTableSize:interface 数量

interfaceTable:interface 表,指向 interface 实现

和 android 相比,Kotlin Native 将 interface 方法和 abstract 方法都通过 interfacetable 存储,android 是分开存储的

内存回收(GC)

GC 有三种类型,默认 pcms,cms 需要手动配置

cms 是并发标记的,只在遍历 gc root 时暂停线程,性能最好

Stms 需要 stop the world 暂停线程,性能很差

默认 pcms 可以支持多线程 gc,也会 stop the world 暂停线程

由于 cms 性能最好,目前 KMP 项目里面默认使用 cms

cms 类型主要包括几个功能,在在 gc root 收集完成后,会 resume the world 唤醒线程

StopTheWord 所有线程将线程暂停执行

collectRootSet 收集 gc root

resumeTheWorld 唤醒线程

Mark 会根据 gc root 标记存活对象

processWeaks 处理 weakReference

heap.Sweep 释放非存活对象

finalizerProcessor 调用对象 finialize 方法,之前会收集所有线程的 finalize 对象

和 android 相比

heap 默认10M,android 是大对象/小对象各512M,导致比较容易触发 alloc gc,目前已经优化

concurrent gc 通过定时10s触发实现,在空闲时容易造成 cpu 浪费,目前已经优化

cms 目前不会做内存碎片整理,会导致内存占用过高,目前在优化中

cms mark 阶段产生的对象都是存活对象

gc 不支持分代,目前已经优化

小结

Kotlin Multiplatform 在经历了这么多年迭代后,目前现在已经是一个相对成熟的解决方案了。虽然在内存管理方案还有一些瑕疵,但其『IR 翻译成 Native』设计理念使得整个系统的性能上限很高,理论上能达到接近原生的执行性能。而 Jetbrain 的号召力也使得整个研发生态非常有想象力,目前 androidx 已经在开始逐步适配 KMP 中,可以预见的将来会非常有潜力。

来源:字节跳动技术团队

相关推荐