Kotlin Multiplatform 初探

引言

作为一个Android开发,我们都对Kotlin十分熟悉了。得益于Kotlin诸多的优良特性和极好的与Java的互操作性,Kotlin已经成为Android开发的首选语言。

我们都知道,Kotlin在Android上会编译成JVM字节码,从理论上来说,JVM本身就是跨平台的,所以Kotlin在这个维度上本身就是支持跨平台的。但是这种跨平台只能在可以运行JVM的设备上实现,对于无法运行JVM的设备或者环境,如iOS、浏览器上运行。同时,即使是在JVM本身的优化水平和硬件水平已经相当高的今天,在一些环境下,JVM和平台原生代码的性能相比,仍然存在很大的差距。

但以上的限制和问题,仅仅是针对运行在JVM上的Kotlin,也就是Kotlin/JVM来说的,Kotlin语言本身就是支持跨平台的,本篇文章我们就来一起探索Kotlin跨平台也就是Kotlin Multiplatform(KMP)相关的知识。

什么是KMP

官网简介来看,Kotlin官方对于KMP的简介如下

Reuse Kotlin code across Android, iOS, web, desktop, and server-side while keeping native code if needed.

对于这句话,我们可以提炼出两个重点

  1. KMP可以运行在 Android iOS web desktop server多种平台上
  2. KMP可以同时保留native code

仅仅从这两点,我们就能看出KMP的独特之处

  1. 能够支持如此多的平台,这是当前所有的跨端方案(RN只支持Android、iOS双端,flutter不支持server)都不具备的
  2. 能够支持和具体平台原生代码的交互(区分于我们常见的bridge方案,这里的英文原文使用的是keeping)

基于以上两个特点,KMP就拥有一些其他跨平台框架所不具备的独特优势

KMP的优势是什么

我们依旧以官网上的内容来切入

在跨平台的同时保证性能和灵活性

Shared Kotlin code compiles into platform binaries, integrating seamlessly into any project. Along with language features that allow you to utilize platform-specific APIs, there's no longer a need to decide between native and cross-platform development. You can have the best of both worlds at the same time!

从上面这句话,我们可以知道,Kotlin跨平台的代码最终编译的产物是对应平台的二进制文件,也就是说在JVM上是class文件、浏览器上是JS文件、Windows上是exe文件等等。

这就与我们其他的常见的跨端框架有非常大的不同。像RN、Lynx以及Flutter这种框架,他们都需要在不同的平台上引入对应的引擎或者VM来执行代码,这就免不了有额外的性能开销。除此之外,当需要与原生逻辑进行交互时,他们都需要通过桥(bridge)来实现,这里通常也都会有额外的通信性能开销。

而对于KMP来说,这种问题就完全不存在了,因为在各个平台的产物都是对应平台的native code对应的产物,所以理论性能完全一致。同时在与native code互调时,只要双方产物能互相找到想要调用的对方的方法签名,就可以无痛地进行互调,没有任何额外的开销。

除此之外,对于一个成熟的项目,引入任何一个跨端框架都是有稳定性、开发流程、包大小等一些列方面的成本的。而KMP相对来说成本较小,更像是引入了一个对应平台的lib,而不是一个成套的框架。

适用于任何项目

基于上述的工作模式, KMP在接入方面十分的灵活。

你可以只在部分多端一致性需求强的部分(加解密算法、计算逻辑、通用的业务model)使用,也可以所有的逻辑都使用KMP来编写。

除此之外,KMP还可以实现UI跨平台。作为Android开发,我们可能都已经或多或少的听说过compose,但你可能不知道的是,compose也是支持跨平台的。

只要我们在KMP上使用compose来编UI代码,就可以实现多平台上UI的共享。

下面是KMP官网上的一个示例图。

一个简单的demo

上面讲了那么多,让我们来简单看一下,在实际项目中,KMP是怎么编写的

创建项目

不同于平常我们创建项目的方式,为了创建KMP项目的灵活性和提供更多模版,IDEA并不支持直接在其中创建KMP项目,而是要在对应的网站中去进行项目的初始化和下载

kmp.jetbrains.com/#newProject

它提供了两种创建KMP项目的方式

一种是手动去选择你要支持的平台,勾选之后进行对应代码的下载。

第二种则更简单,对于一些常见的选项,Jetbrains官方给了三套模版,从上到下依次是

  1. 支持逻辑和UI共享的Android iOS双端模版
  2. 只支持逻辑共享的Android iOS双端模版
  3. 只支持逻辑共享的多端模版

基于上述的选项和模版,我们可以快速创建一个KMP的项目

在这里我们选择第三个模版来做下面的演示,因为Compose for iOS还处于alpha阶段,相对来说还没有那么成熟,同时带UI的模版只是在只带逻辑的模版基础上增加了UI层Compose的支持,写法上并没有太多的区别。

项目结构

我们直接在IDEA中导入上一步中下载好的代码模板,打开后可以看到如下的结构

其中**convention-plugins**是一些方便我们发布产物的脚本,暂且忽略

主要的就是library目录,我们可以看到library目录下有很多目录,其中主要的就是commonMain,这里存放的是所有平台上公共的代码,而其他的平台上独有的代码,则存放在对应的platformMain目录。

library目录下的build.gradle文件中,我们可以通过gradle去修改我们支持的平台,以及添加依赖。

因为我们平时开发都是在Arm的Mac上,为了方便我们做进一步的演示,我们添加一个macosArm64()在其中。

需要注意的是,在红框圈出来的部分,仅能添加同样是支持多平台的库的依赖,如果我们需要针对特定的平台添加特定的依赖,需要添加在对应的平台上,以Android为例,我们需要添加

kotlin 复制代码
        val androidMain by getting {
            dependencies {
                implementation("")
            }
        }

代码编写

这个模版中提供了一个简单的计算斐波那契函数的例子,为了简单理解,我们稍微改动一下,改为只计算两个数字之和,不再搞这么复杂了

kotlin 复制代码
expect  val  firstElement: Int  expect  val  secondElement: Int   fun  twoSum (first: Int , second: Int ) : Int {  return  first + second }  fun  main () {  val  result = twoSum(firstElement, secondElement) println(result) } 

可以看到,这个写法和我们平时写Kotlin并没有什么区别,我们直接运行,可以正常输出得到结果

但是如果我们进一步尝试,比如想要输出一下当前的时间时,你会发现编译会报错

这是因为,System是JVM提供的类,而我们现在是在写跨平台的通用代码,所以没有办法调用到这种单一平台上的API。

但是如果是在对应平台进行相应代码的调用,就不会报错

那既然存在这种问题,那Kotlin就肯定会给我们提供一种不同平台间交互的方式,毕竟我们不能指望所有的上下游依赖都使用KMP来实现一遍,这是不现实的。

而这就是expectactual关键字的作用。

在上面的例子中,我们在第一步只是声明了*firstElement secondElement*两个变量,并没有给他们实际的值,而程序依然可以运行,就是因为我们在对应的JVM平台的代码实现中,又声明了它们两个值分别是2和3,因此我们才有了5这个返回值。

而在不同的平台中,我们也可以给它返回不同的值,比如在Mac平台上我这样声明

kotlin 复制代码
actual  val  firstElement: Int = 7
actual  val  secondElement: Int = 8

那么理论上在Mac上这个程序的运行结果就会是15,我们将会在下面的部分验证这一点。

到此其实我们就已经完全了解了KMP代码编写的要点,足够去进行KMP的开发了。

编译产物

由于默认的模版只提供了JVM的执行入口,如果我们想验证其他平台的执行情况,就需要手动在gradle中添加执行入口,我们以Mac平台作为演示,其他平台也是类似的,完整的文档可以参考Build final native binaries | Kotlin

我们修改Mac的部分为,代表我们要为Mac平台添加一个可执行的二进制产物任务

javascript 复制代码
macosArm64() {
binaries {
executable {

}
}
} 

之后执行./gradlew :library:runDebugExecutableMacosArm64命令,就可以得到如下结果

可以看到不同于JVM的5,我们得到的结果是15,这也验证了我们上面的说法

那么这个编译的产物是什么呢?

我们可以在build目录下找到它,最终生成的就是一个二进制的可执行文件,我们可以直接在控制台中运行它,只不过与常见的可执行二进制文件不同,它用.kexe来表示它是一个Kotlin生成的可执行文件

上面是可执行文件,那如果我是想要一个库呢

如果我们直接尝试运行./gradlew :library:compileKotlinMacosArm64命令,理论上来说,在Mac上我们会得到一个.dylib文件,在Linux上我们会得到一个.so文件,但当我们尝试过之后会发现,并没有类似的文件产生,而是生成了一个klib文件

官方文档是这么描述的

By default, a Kotlin/Native target is compiled down to a *.klib library artifact, which can be consumed by Kotlin/Native itself as a dependency but cannot be executed or used as a native library.

可见这个文件仅仅是为了给其他KMP的模块使用的,那么我们应该如何得到我们想要的最终产物呢

同样是修改gradle,为了对比,这次我们给linux也加上对应的配置

markdown 复制代码
macosArm64() {
    binaries {
        executable {

        }

        sharedLib {

        }
    }
}
linuxX64() {
    binaries {
        sharedLib {

        }
    }
} 

之后我们分别运行./gradlew :library:linkDebugSharedLinuxX64./gradlew :library:linkDebugSharedMacosArm64,就可以在对应的文件夹下看到我们想要的产物了

这样对应的平台就可以拿到这个lib去进行实际的依赖和调用

实现原理

跨平台代码生成

这一部分会涉及到一些编译原理相关的知识,相对来说较为复杂,我们尽量简短地来描述

通常来说,编译器对我们来说就是一个黑盒,我们输入源码,它给我们输出对应的产物。但是我们如果想要了解Kotlin多平台的魔力,就必须要理解Kotlin编译器的运行原理

用一张图可以大概地进行表示

在Kotlin编译器中,也分前端和后端,但是这个前后端的含义与我们平时的含义有些差异

在前端中,主要有两个部分

  1. 语法分析 在这一过程中,主要是分析代码语法的正确性,举个例子,如果你的if语句括号没有闭合,在这一步就会被检查出错误。如果没有问题就会生成语法树进行下一步。
  2. 语义分析 在这一过程中,主要是分析整体代码语义的正确性,再举个例子,如果的对象调用了不存在的方法或者方法的入参参数个数或者类型不对,都会在这一步被检查出错误。如果没有问题就会把上一步已经生成的语法树和经过分析之后的语义信息传递给后端进行产物的编译工作。

在后端中,也分为两个部分

  1. IR 代码生成,IR代码比起源代码更加通用和便于优化,但这一步并不是必须的,即使不生产IR代码,仅依靠语法树和语义信息也可以进行下一步。
  2. 机器码生成,这一步就是真正的将源码转换成我们平时看到的真正产物的一步。

有了以上的知识,我们就不难理解Kotlin的做法,在前端阶段,不论哪个平台都是一样的,因此可以通用,而到了后端,只需要编写不同平台的机器码后端,就可以实现对应的效果

你可能已经注意到了图里的old,这里涉及到了一些Kotlin编译器迭代历史和设计相关的背景,但无论是old还是new的后端,它们整体的流程是没有改变的

更详细的内容你可以在下方的视频中了解

www.bilibili.com/video/BV1yL...

www.bilibili.com/video/BV1vL...

compose的跨平台渲染

Compose的渲染,最底层都是基于Skia的,但在不同平台间的差异可以大概由下图表示

在Android中,由于系统底层的渲染本身就是基于Skia的,所以所有的渲染都是通过代理给Android原生的Canvas,再由系统底层的Skia进行渲染。

而对于其他平台,JetBrains封装了github.com/JetBrains/s... 来方便上层对Skia在不同平台间的操作,因此在其他平台上,虽然也是通过Skia来进行渲染,但Skia需要被额外打包到对应的平台产物中。

更详细的内容你可以在下方的视频中了解

www.bilibili.com/video/BV1YN...

KMP生态

作为JetBrains力推的跨平台解决方案,当前JetBrains已经提供了一些开箱即用的KMP库,如网络请求库 Ktor、序列化库 kotlinx.serialization、时间日期库 kotlinx-datetime,以及我们已经很熟悉的协程等。

社区也有一些精品开源项目,比如Square出品的IO库 okio 和数据库库sqldelight等。

除此之外,Google也在将一些我们Android中常用的开发组件迁移到KMP上,这对一个Android开发者来说无疑是一个好消息。

业界案例

在Kotlin的官网中,列举了一些已经在使用KMP的公司,但全是海外公司

但目前在国内,也还是有很多公司已经在生产环境使用上了KMP,下面是我找到的一些分享案例

www.bilibili.com/video/BV12a...

其中美团的这一案例分享的较为细致,对我们有很大的参考意义

www.bilibili.com/video/BV15K...

总结

本文对KMP做了一个整体上的简介,希望大家能通过本文能够理解到KMP是什么、KMP能做什么,从而开阔大家的思路。对KMP这一个并不是很新的新技术有一个简单的了解。

相关推荐
大白要努力!1 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟2 小时前
Android音频采集
android·音视频
小白也想学C3 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程3 小时前
初级数据结构——树
android·java·数据结构
闲暇部落5 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX7 小时前
Android 分区相关介绍
android
大白要努力!8 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee8 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood8 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-11 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记