工程化视角的 Kotlin Multiplatform核心解读及优化

本篇为KMP技术的技术及实践系列文章的第二篇。在这篇技术文章中我们会以百人移动研发团队的工程化视角,探讨Kotlin Multiplatform的核心技术及优化。

Kotlin: 语言与编译

人们在用自然语言沟通时,内容可以不明确,甚至小的错误,而听的人仍然可能理解说的人想要说的内容。但电脑不同,电脑“只做被告知要做的事”,无法理解程式设计者想要写的程式。语言的定义、编程以及编程输入的组合需完整定义程式执行时的外部特性。 而程序语言正是人类和计算机的桥梁, 顺着这个逻辑,我们把我们日常的编程工作和一些核心概念结合起来。

  • 人控制计算机,所以编程语言是给人写的。那么自然就要符合人类的思维习惯,例如面向对象,函数式编程等。这也是为什么有那么多的编程语言的不同之处。

  • 计算机世界只有0/1,而交付0/1对人类来说在现代实在太困难了,所以我们发明了了指令集架构,发明了汇编,发明了各种例如JVM的字节码。而这些也就是计算机所需要的输入。

  • 人类的思维和计算机的所需要的输入之间有一个翻译的过程,这个过程就是编译器。编译器的目的就是把人类的思维翻译成不同level的计算机所需要的输入。

  • 我们通过编程语言与计算机沟通,那么自然希望这个语言是一种扩展性强的,让我们不被语言语法本身所限制,通常对人类抽象层面越高的语言他们的表达能力反而更弱,这也是插件系统的重要性所在。插件主要的作用就是在不增加原语言复杂度的前提下扩展出更强的能力,当然并不是所有的语言都通过插件体系来实现核心能力(例如类似的compose&swiftui,前者是通过插件生态扩展能力,后者(虽然在swift5.9 之后支持了macro的插件体系)依然是通过为语言本身增加更多的表达能力来达成)

接下来我们就结合KMP从编程语言的角度来刨析实际工程化中的实践

语言(language)

默认可见性为public😭

可见性/Visibility modifiers(https://kotlinlang.org/docs/visibility-modifiers.html)在工程化上是一件至关重要的事,因为可见性的控制直接影响到了代码的复用性和维护性。Kotlin 的设计哲学是你不需要它时,它不会打扰你;你需要它时,它就在那里 。默认的 public 可见性体现了这一哲学,即不强制开发者在不需要时使用可见性修饰符。但是当Kotlin进化从JVM进化到Multiplatform后,针对不同平台的可见性就有自己的特点了。例如当链接到Native时,哪些符号需要做C/Objc binding ?例如编译到JS后什么需要导出到模块?这些都是需要开发者自己去考虑的。导致的结果就是有些是通过Kotlin自身的一些annotation例如@JsExport有些是通过编译器参数-Xexport-library来实现的。例如语言层面的Visibility叉乘无论是annotation还是编译参数的组合,这会要求开发者更多的感知这些语言层面的细节,从而违反了他原来的设计哲学。

编译器(compilers)

三个编译器

上文我们提到编译器的工作是交付计算机可以理解的输入,而大多数传统跨平台语言的做法主要有两种。1. 类似C++、Rust直接输出不同CPU架构的机器码。2. 类似Java、C#输出字节码,然后通过不同的Runtime来解释执行。前者的最大问题就是和原生平台的语言交互不畅,毕竟是两个层面的语言(例如JVM平台上通过JNI与Native交互),后者的问题就是性能问题,无论如何都是过了一层中间层。KMP在这的选型是通过三个编译器来实现的,分别是Kotlin-Jvm、Kotlin-Native、Kotlin-Js。可以直接输出原生平台的机器交付产物,在原生友好性与一致性之间达到了一个平衡点。优点的同时也一定会伴随痛点,例如不同平台的不同行为定义,经常需要通过在common代码中定义一些其他平台毫无关心的annotation来实现。

图片

Kotlin/Native

  • ${KOTLIN}/bin/kotlin-native是一个把Kotlin Code转化成不需要虚拟机从而直接运行在目标平台上的编译器。他主要包括了基于LLVM的Frontend的实现以及对Kotlin的标准库的Native实现。

  • ${KOTLIN}/bin/cinterop是一个把C/ObjC语言的头文件转化成可被Kotlin/Native调用的 .klib 的工具。

  • ${KOTLIN}/klib/platform是Kotlin/Native SDK中帮助开发者默认 binding 的所有平台能力,例如android/ndk ios/Foundation linux/posix等。

优点:

  1. 在这两个工具以及默认binding的情况下,我们发现在实际开发的工程化中,语言之间的互操作变得无比的流畅!

缺点:

  1. 由于平台的binding是放在kotlin中的,导致平台的能力层面与kotlin版本之间产生了绑定,例如ios/UIKit是内置在Xcode的,也变相产生了kotlin版本与Xcode版本之间的绑定。

  2. 由于编译到机器码的原因编译时间相对JVM较长,同时由于目前编译器层需要完整的依赖链,导致类似implementation deps这种特性无法实现。从而降低了增量编译速度,劣化了体验。

Kotlin/JS 😭

双向interop

由于Kotlin和TypeScript/JavaScript这两个语言在语言层面有非常大的差距,例如literal types(https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) 在Kotlin中是没有对等的, 例如Long类型在标准库中是没有对应的JS类型的,例如Error的处理在两个语言中是完全不同的,这就导致了在Interop的时候需要通过大量的转换手段来解决这个问题。而社区上也并没有一个很好的解决方案。目前相对(基本不)可用的两个工具分别为dukat(https://github.com/Kotlin/dukat) 和 karakum(https://github.com/karakum-team/karakum)。K/JS在B站目前主要用于鸿蒙,而鸿蒙自身也不太稳定,导致两个不稳定因素叠加更不可控。目前我们的做法就是基于 karakum 进行问题的修修补补,做到勉强可用。

产物缺少精准strip能力

在K/N中提供了类似-Xexport-library的能力供开发者决策toplevel模块的导出,从而基于这棵树来做unused strip。但是在K/JS中并没有这样的能力, K/JS会导出所有依赖树中声明为@JsExport的顶层模块的树,看似也是提供了Export的声明但其实是并无法解决问题。例如在不同variants的情况下针对相同模块有不同的依赖树, 例如a->b在项目A中需要导出a ,在项目B中需要导出b ,这就导致我们不得不在两个模块中同时声明JSExport而这会导致在A项目中看到不关心的b模块。又由于b模块作为了TopLevel会导出大量与A无关的体积代码。

同symbol name的冲突

  • JsExport-declaration-name-clash-with-ES-modules(https://youtrack.jetbrains.com/issue/KT-60140/MPP-JS-conflicting-JS-signatures-are-not-reported-and-invalid-JS-code-is-emitted

无法以同ir粒度的模块切分.d.ts

当前K/JS 的TS的生成是基于binary也即是一个产物一个.d.ts ,把Kotlin所有的模块合并在了一起,即使通过-Xir-per-module拆分了 .js 但是 .ts缺依然没拆分。导致K/JS 的复杂工程无法以模块的方式提供给其他前端项目使用(例如鸿蒙),只能以聚合产物的方式交付。

产物(artifacts)

klib

图片

在KMP世界中和过去基JVM的的产物发生了一些变化,即定义了一种新的 lib 结构.klib 我们基

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值