摘要:这时候跨端方案就成了香饽饽,而Kotlin家族里的KMP(KotlinMultiplatform),就是这么个能帮大家“一码多端”的角色。
现在做开发的,尤其是搞多端开发的,谁没为重复写代码头疼过啊?
Android一套、iOS一套,有时候还得弄个Web端,不光人力成本上去了,不同端还总出兼容问题。
这时候跨端方案就成了香饽饽,而Kotlin家族里的KMP(KotlinMultiplatform),就是这么个能帮大家“一码多端”的角色。
今天咱们就好好唠唠它,看看它到底是怎么干活的,有啥过人之处,又有哪些地方让人犯难。
首先得说清楚,KMP跟咱们常见的Java、React跨端方案不一样。
那些方案大多靠虚拟机中间转译,代码跑起来总绕个弯。
KMP不搞这一套,它直接把Kotlin源码编译成目标平台的原生代码,比如给Android就编成Java代码,给iOS就编成C++代码,给Web就编成JS代码。
这样一来,省了重复开发的功夫,听着就挺省心。
不过它最让人眼前一亮的,还是性能。
原文里说,因为直接编译成原生代码,跨语言调用(也就是FFI)的时候几乎没性能折损,跑起来跟原生系统的代码差不多快。
这时候可能有人会问:真这么神?要是调用特别复杂的跨语言接口,比如传一堆复杂对象,会不会藏着咱们看不见的开销?其实按原文的说法,还真不会。
因为它少了虚拟机那层“中间商”,哪怕是简单的整数运算,编译后跟直接用C++写的速度也没差多少,性能优势确实实打实。
但KMP也不是没短板。最麻烦的就是老库的问题,早期Kotlin是跟着Java、Android走的,很多老的二三方库设计时就没考虑过跨平台,只能在JVM(比如Java、Android)上用,放到iOS上一编译准报错。
还有系统接口也分“地盘”:kotlin.*、kotlinx.的接口能跨平台用,java.、sun.*的只能在JVM上跑,android.*的仅限Android,androidx.*更麻烦,得查文档才知道支不支持。
这时候有人可能会想:现在生态是不是好点了?新库应该都支持多平台了吧?
确实现在不少新库开始适配KMP,但老项目里还是一堆JVM、Android的依赖啊!
要是当初没设计隔离层,想把Android代码直接转成其他平台的产物,基本不可能,还得花大力气做抽象改造,这也是没办法的事,早期库的局限性就摆在那儿。
再说说KMP能跨平台的核心,Kotlin编译器。它用了“前后端分离”的思路:前端负责把Kotlin代码解析清楚、分析明白,后端负责把前端处理好的东西翻译成目标平台的代码。
以后想支持新平台,比如鸿蒙,不用改前端,加个新后端就行。这时候可能有人质疑:前后端分离会不会出岔子?
比如不同后端对前端的产物理解不一样,导致同一行Kotlin代码在不同平台编译出的结果有差异?
其实原文里也说了,前后端职责分得特别清,前端输出的产物是统一的,只要后端按标准处理,兼容性问题就能控制住。
而且优化工作也放在后端,针对不同平台的特性做优化,反而更灵活。
咱们重点聊聊KotlinNative,毕竟JVM大家都熟。它的编译入口要么是Gradle任务,要么是命令行里的konanc,最终产物分四种:Klib(有点像jar、aar,存的是KotlinIR信息)、ObjCFramework(给iOS用的)、Binary(缓存或者可执行文件)、CLibrary(动态库或静态库)。
比如编译Klib,就是把.kt文件转成Klib,后续link的时候再用C++工具链处理;另外三种产物,都是把多个Klib聚合起来编译,有点像C语言的link或者Android打包apk,区别就在于最终出的东西不一样。
最有意思的是IR转换。原文给了个例子:一个HelloWorld类里有个helloFun1函数,接收两个Int参数,返回它们的和。
这段代码编译成LLVMIR后,再翻译回C风格代码,逻辑跟原来一模一样,就是函数名特别长,比如“kfun:com.demo.kmp.HelloWorld#helloFun1(kotlin.Int;kotlin.Int){}kotlin.Int”。
这时候有人可能会问:把Kotlin代码转成LLVMIR,会不会丢了Kotlin的特性啊?比如协程、空安全这些,转完还能用吗?
其实不用担这个心,原文里说编译流程里有个“Lowering”步骤,会处理语法糖、内联这些,而且运行时也支持协程,特性一点都不会丢,执行效率还高。
KotlinNative还有套自己的运行时,打包在最终产物里,负责内存回收、异常处理、线程管理这些活儿。
内存分配器现在默认是Kotlin自己开发的custom,之前的std和mimalloc已经去掉了,未来会重点优化custom。
跟Android比,它没有synchronized关键字,得用atomicFu代替;GC方面有三种类型,cms性能最好,只在收集GCroot的时候暂停线程,默认用的是pcms。
不过早期也有问题,比如堆默认才10M,比Android的512M小太多,容易触发GC,好在现在已经优化了。
还有cms不做内存碎片整理,会导致内存占用高,目前也在改进中。
可能有人会问:既然custom是后来才成默认的,是不是早期有缺陷啊?
其实去掉其他分配器是为了聚焦优化custom,虽然现在还有点小瑕疵,但一直在变好,比如之前并发GC每10秒触发一次,空闲时浪费CPU,现在也解决了。
这种现象看着能解放人的大脑,但从现实中说,也让更多的岗位从此被完全革除。
KMP的发展现在已经超过了人脑,未来必定能掌握比人类还要多的技能。
这是科技发展的必然,只是我们现在正在经历被革除的尴尬时代。
总的来说,KMP这么多年迭代下来,已经挺成熟了。
虽然内存管理还有点小问题,但它“把IR翻译成原生代码”的思路特别棒,性能上限高,理论上能接近原生执行速度。
而且Jetbrain的号召力摆在那儿,现在androidx都开始适配KMP了,未来的生态肯定会越来越好。
对于开发者来说,KMP确实能解决不少跨端的痛点,虽然改造老项目麻烦点,但如果是新项目,从一开始就用KMP设计,能省不少事。
相信再过一段时间,随着内存管理的优化和生态的完善,KMP会成为更多多端项目的选择,毕竟“一码多端”还不牺牲性能,这种好事谁不爱呢?
来源:江语迟