Kotlin 跨平台实践经验分享
引言
随着研发技术的发展,跨平台混合开发越来越流行,Flutter、React Native(以下简称 RN) 这样的框架有越来越多的公司和项目开始使用,Flutter 近两年的迭代速度逐步加快,Google 官方的开发工具 Android Studio 也对其做了许多优化。Flutter 和 RN 专注于多平台构建同一套 UI,已经接受过许多项目的检验,表现还不错。但在面对一些数据处理、后台任务、原生调用的场景时,它们的表现都不尽人意。而这时 Kotlin Multiplatform(以下简称 KMP) 的出现,似乎可以来解决这个问题,如果能够解决,理论上 KMP 是否可以做到不仅 UI 跨平台,全项目都跨平台呢?为了解答这个问题,我开始在项目中实验是否可行。与此同时(2020.9),KMP 也从 Experimental(实验性的)功能升级到了 Alpha(内测的)功能(Kotlin 1.4.0),KMP 的功能、性能和稳定性都改进了许多,表现出了良好的潜力。
参与实践的项目一共有 4 个,类型分为两种,一种是利用 KMP 来构建独立的多端项目。一种是使用 KMP 来构建公共库以供现有的项目使用。我将分享这两种类型的项目中的实践经验。
一、使用 KMP 构建独立的多端项目
1.1 项目背景
这是第一次使用 KMP 研发独立的项目,主要功能是做数据采集的 SDK,虽然项目并不直接生成应用程序(apk、html),但拥有独立的逻辑闭环和生命周期控制。
项目包含 Android、iOS、Web 三端,没有 UI 界面。Android 部分生成 .aar
,iOS 部分生成 .framework
,Web 部分生成 js、ts
可上传到 npm 仓库。
1.2 项目架构
项目主要结构如下:
公共模块
公共模块是三端一样的代码,我将能抽离出去的代码都写到了公共模块,包括主要的业务逻辑、数据处理、网络请求等代码。将存储和 IO 的具体实现放到了原生模块,公共模块只需要调用其统一的接口。网络请求框架使用的官方提供的 Ktor Client,可以直接在公共模块使用。Android 模块
Android 模块包括在 Android 设备上的存储和、IO 以及 SDK 接入入口的相关代码,并抽象成相应的接口给公共模块调用,存储使用的是 SharedPerference 和 File。Ktor Http 引擎使用的是 OKHttp。iOS 模块
与 Android 模块的内容大致相同。Web 模块
与 Android 模块的内容大致相同,存储采用的 localStorage。
项目配置的是 1.4.0 版本的 Kotlin,并使用了 1.4.0 的新特性「IR 编译器」,可以将 Web 模块的代码生成 Typescript 定义文件,方便接入方使用。
参考链接
这样做的优势在于可以实现一套代码多端使用,并且可以保证主业务逻辑的一致性,从代码层面上提升研发效率。
1.3 踩坑
IR 编译器不稳定
IR 编译器在发布的第一个版本存在诸多问题,例如和 kotlinx.serialization 一起使用时,会出现编译不通过的情况,这将导致在项目中使用 kotlinx.serialization 后,无法正常 IR 编译器的@JsExport
功能。iOS framework 编译时间过长
KMP 工程生成 framework 文件需要使用 Xcode command line,这期间需要耗费很长的时间,在这个项目中,Android 和 Web 构建的时间加起来仅有两分钟不到,而构建 iOS framework 需要接近 10 分钟。Ktor Client 使用不方便
项目使用 Kotlin 官方提供的网络请求库 Ktor Client 发送业务的网络请求,但其功能有限,不支持统一拦截器,实际使用体验并不好。
其中还有一个小插曲,项目中使用 Ktor Client 往 Aliyun OSS 传文件,采用 postObject 的方式,但 Ktor Client 上传文件的报文始终通不过 Aliyun 的校验,提客服工单检查之后,问题竟然是 Ktor Client 会自动的去补充一些 post formData 请求的报文,导致无法通过 postObject 上传文件。Kotlin coroutines 协程在集成进 iOS 项目后崩溃
项目使用协程进行统一的后台任务管理,同一套代码在 Android 上可以正常使用,打包成 iOS framework 使用时会产生崩溃。
1.4 小结
KMP 在独立的项目研发场景表现一般,虽然结果是可以使用起来,但在一路上踩坑许多,暴露出了 KMP 目前在独立项目的研发场景下稳定性不够、生态不够好,导致研发效率反而降低同时还拥有新技术的不确定性风险。
二、使用 KMP 构建公共库
2.1 背景
我们想在现有的项目中尝试 KMP,将一部分新功能的业务逻辑和数据模型抽离至的 KMP 项目,不包含网络请求、IO、数据库等原生操作,再用原生项目引用 KMP 项目生成的包,达到一套代码多端使用的目的。尝试这种方式的项目有 3 个。
2.2 架构思路
项目主要结构如下:
公共模块
我们将一些新功能的纯业务逻辑、常量定义和数据模型写到了公共模块,可以供服务端和客户端一并使用,达到全端的统一。各个端的模块
在这个架构中,各个端的模块中不会包含过多的代码,一般会包含对模型的处理以及对业务逻辑的封装,以供接入方更好的使用。
这样做的优势在于可以对现有的项目影响较小,仅仅只是一小部分新功能的业务抽离出去使用 KMP,即使发现无法解决的阻塞性问题,也可以及时更换为原生代码。如果能够稳固运行,后续也很好扩展。
2.3 踩坑
能抽离出去的逻辑有限
新功能能抽离出去的公用纯业务逻辑很少。在实践的时候,许多看似可以抽离出去的业务逻辑实际上都会带有一些与现有项目的耦合,并且多端统一的逻辑往往是有限的。调试阶段,编译打包跨平台库消耗时间过长。
在研发和调试阶段,需要频改动公共库,有的时候甚至只是改动一行代码都需要重新编译项目,导出对应平台的包再让原生项目依赖,整个流程比较消耗时间,影响研发效率。使用时相比原生代码体验有一定差距
在使用这种架构的项目中,尝试过导出 framework(iOS)、js(Web)、js + ts(Web Kotlin 1.4.0)、dll(win)。其中:
- iOS framework 包在 iOS 中的使用体验一般,能够正常使用,但会繁琐一点,和原生代码还有差距的。
- js 在现有 React Web 项目中使用体验较差,由于缺少 ts 定义文件,所以使用的时候只能记住对应的函数和变量。
- js + ts 在现有 React Web 项目中体验不错。
- dll 在 Windows 开发中(C#)体验很差,虽然可以使用,但加载 Kotlin 生成的动态(和静态)链接库都很繁琐,使用的成本增加了不少。
2.4 小结
使用 KMP 构建公共库的形态总体来说使用体验一般,虽然可以做到多平台兼容,但是在 3 个项目实施后并没有达到很理想的效果,主要体现在两点,分别是:
- 开发效率提升不明显
- 由于抽离的代码较少,所以代码统一所带来的稳定性提升、易维护的价值较少。
- 部分端因使用体验较差用原生代码重写。
三、总结
3.1 目前不适合在生产项目中使用
本次 KMP 项目的实践虽然最终都能够成功运行,但在过程中遇到了较多的坑,结合目前官方 KMP 依然处于测试(Alpha)阶段,社区资料和轮子较少,所以得出的结论是目前不适合在生产项目中使用。
主要问题如下:
- 使用期间的不确定坑较多,遇到疑难的坑一般只能向社区求助(官方社区 YouTrack 或 Github)。
- 社区资料和轮子较少。
- 与原生代码的契合度还需要提高。
- 开发效率提高不明显。在实际开发中,虽然各端一起使用 KMP,但依然需要足够多的原生开发知识作为支撑才能进行开发,且需要最好每位参与开发的人员都会 Kotlin。
- KMP 生成的包依赖较多,会增加最终应用体积。例如在 KMP 研发业务库的场景,原生项目本身没有 kotlin 相关的依赖,但使用就必须得依赖 kotlin 基础库以及使用到的 kotlinx 库
- 性能问题。我们发现在一个 iOS 原生项目使用多个 KMP 生成的 framework 时,会依赖多份 kotlin 基础库,并且这些库在应用启动时就会加载,对应用的性能产生一定影响。
3.2 KMP 的未来
虽然当下 KMP 的表现在生产项目中不尽人意,但作为技术的探索,KMP 目标做到在所有平台下都能轻松的使用 Kotlin 开发,纵观 Kotlin 的发展史,KMP 的发展越来越迅速,目前已经到测试阶段,且所应用的场景确实为混合开发的痛点之一。所以我认为 KMP 的未来是很有前景的,一起期待吧。
3.3 新技术在团队推行的考虑
本次 KMP 的实践,也是一项新技术在团队推行的尝试,我认为这其中还有许多可以优化的地方和值得考虑的问题。这里聊一聊我的看法,未来其他新技术实践时可以参考。
团队内新技术的推行,我觉得需要考虑以下问题:
- 新技术是否成熟、稳定性、社区资料、生态怎么样,是否具备接入生产项目的条件,对新技术的原理掌控如何。
- 与当前团队的技术栈该如何结合,有哪些风险和价值,价值是否大于风险,如何最小化验证。
- 最小化验证的结果如何,是否可以支撑与当前的团队技术栈结合。
- 如何设计落地方案,评估风险与容错机制,保证稳定优先。
- 如何执行落地方案,如何保证顺利执行、预防事故以及事故解决方案。
- 无论是成功落地还是放弃,如何储备技术能力。
以上的推行方式采用循序渐进的模式,虽然标准很严苛,但是让新技术在团队内落地得到很好的把控,避免因为实际尝试后发现致命性问题而造成返工。即使新技术在中间阶段验证之后发现无法落地,也可以作为技术储备,提升团队的技术能力。