KMP(Kotlin multiplatform)是 Kotlin 语言的一项重要特性,允许将 kotlin 代码运行在不同平台上,通过『一码多端』的方式来节省成本。
KMP 的优势和限制
相较传统的跨平台框架而言,由于 Kotlin 会将代码编译成目标平台原生代码执行(可以简单理解为将 Kotlin 源码翻译成 java/c++/js 代码),其最大的优势在于进行 FFI(跨语言调用)时几乎没有性能折损,并且执行性能接近于原生系统。
由于早期的 kotlin 是基于 java / android 平台,这些 kotlin 二/三方库在设计时候也不可能考虑过跨平台。考虑到这些情况,kotlin 在编译时使用了 Target Platform概念,即 kotlin 每个类 / 方法 都是有对应平台的,早期的的 java / android 二三方库只属于 jvm 平台,意味只能在 java / android 平台调用,在其他平台上调用会编译报错。
所有 kotlin.*、kotlinx.*包名的接口,都是跨平台的。
所有 android.*包名的接口,只能在 android 平台使用。
因此,如果想要将 Android 代码通过 KMP 直接编译成其他平台产物,那基本上是不可能直接成功的。如果没有提前设计隔离层的话,工程中二、三方依赖,以及源码中几乎不可避免的含有 Android / jvm 的平台接口,你很可能需要进行大量的抽象改造才可以完成。
跨平台概述
在实现上,kotlin 编译器使用了前后端分离的思路。
至于 Optimizer,由于不同目标平台的优化方式不同,在 kotlin 编译器中被放在了后端中。
由于 Kotlin Jvm 大家相对比较熟悉,而 Kotlin JS 笔者还没有看,因此本文只着重介绍 Kotlin Native 的相关分析
Kotlin Native 编译入口通常为 Gradle Task 或者命令行(konanc),二者最终执行代码是共通的,最终会根据根据产物类型不同执行不同逻辑。
Klib:Kotlin Native Library,可以简单理解为 Kotlin Native 版本的 jar / aar,只保存了 kotlin ir 信息。
Binary:缓存 / 可执行文件。
produce(Klib)
Klib 解开后结构如下:
produce(Binary/CLibrary/ObjCframework)
各步骤说明:
2. Lowering module && dependencies:将所有依赖库合并,并针对合并后的每个 ir 文件(包括依赖的库的 ir)执行 Lowerings(对 Ir 进行前置优化,比如内联,语法糖处理),每个 lowering 文件需要执行 51 步,每一步都可以在 NativeLoweringPhases.kt 中找到对应的定义。
CodeGen:将 kotlin ir 『翻译』成 llvm IR,这部分主要通过调用 llvm 的 c 函数实现
Post Processing :在和底层依赖库(Runtime)的 bit code 链接前,做一些优化工作,比如去除无用代码。
Compile and link:
调用 lld 将 .o 文件 link 成目标平台汇编代码
假设有如下源码:
package com.demo.kmpclassHelloWorld{funhelloFun1(a: Int, b: Int): Int {return a + b}}
除开一些流转指令、调试指令外、其翻译回 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;}
其主要的『翻译』逻辑如下:
Kotlin 类会『翻译』成 llvm typeInfo 形式,用来记录类名等信息。
Kotlin 函数会『翻译』成 C 函数,差别在于会多一个 ObjHeader* 参数,用作 $this 指针。
Kotlin 运算符会『翻译』成对应的 operator 函数(举例来说,加号(+)会翻译成 add 函数),一些类型(比如基础类型)会进一步通过内联翻译成 C 的运算符。
Kotlin Native 运行时
运行时包括如下几个部分,创建线程或者已存在的线程都可以 initRuntime
globalData 初始化全局变量
workInit 初始化线程消息队列,用于执行协程
内存管理
custom:kotlin 自己开发的内存分配器,也是默认的内存分配器
mimalloc:微软开源的 native 分配器
CreateObject 分配对象,每个对象额外增加16字节内存,包括 objectData/objectHeader
CreateArray 分配 array,每个 array 额外增加24字节内存,包括 objectData/ArrayAHeader,ArrayHeader 12字节按照8字节对齐到16字节
Kotlin Native 把 Array 类型单独拿出来了,Android 认为所有类型都是 Object
superType: 父类
objOffsetCount_:成员变量数量
interfaceTable:interface 表,指向 interface 实现
默认 pcms 可以支持多线程 gc,也会 stop the world 暂停线程
collectRootSet 收集 gc root
Mark 会根据 gc root 标记存活对象
heap.Sweep 释放非存活对象
和 android 相比
concurrent gc 通过定时10s触发实现,在空闲时容易造成 cpu 浪费,目前已经优化
cms mark 阶段产生的对象都是存活对象
小结
Kotlin Multiplatform 在经历了这么多年迭代后,目前现在已经是一个相对成熟的解决方案了。虽然在内存管理方案还有一些瑕疵,但其『IR 翻译成 Native』设计理念使得整个系统的性能上限很高,理论上能达到接近原生的执行性能。而 Jetbrain 的号召力也使得整个研发生态非常有想象力,目前 androidx 已经在开始逐步适配 KMP 中,可以预见的将来会非常有潜力。
custom 内存分配器是 kotlin 自己实现的内存分配器,包括几个部分
Kotlin Native 只支持 Weakreference,不支持 SoftReference
基础类型包括 Byte/Short/Int/Float/String 等,和 android 一致
class 包括几部分
内存回收(GC)
cms 是并发标记的,只在遍历 gc root 时暂停线程,性能最好
cms 类型主要包括几个功能,在在 gc root 收集完成后,会 resume the world 唤醒线程
