再探 Kotlin multiplatform

Posted by Piasy on July 24, 2022
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2022/07/24/Kmpp2/

朋友们好久不见,今天的故事是这样的……

过去一年多的时间里,我刻意在工作之余不碰代码,希望在“生活”上多一些关注,不过最后发现,能持续给我带来快乐的,还是代码。所以前段时间我有了一个项目点子:开发一个投影仪/电视上的 App,可以下载、管理、播放电影,添加一个磁力链接和豆瓣电影链接,就自动把电影下载下来,并且把电影的基本信息从豆瓣页面上提取出来,这样我用投影看电影就会方便很多了。接下来还能添加一些有趣的功能,比如在手机上控制投影,或者 P2P 共享,甚至还能搞个 Web3 相关的点子,又或者加上 RTC 的能力,可以和朋友边看电影边聊天。不过都是后话了,边看电影边聊天还是不太靠谱,真要一起看,就喊到家里看嘛。但至少做这个项目的过程中,可以实践不少技术:投影端可以用 Kotlin multiplatform 开发,手机端可以用 Flutter,还能再玩一玩 Web3 什么的。

所以故事就到了 Kotlin multiplatform 这里,一年半以前我分享了基于 Kotlin multiplatform 的多平台 WebRTC SDK,当时为了单线程消息队列,着实费了一番功夫。这一年半过去,Kotlin multiplatform 有什么新的进展呢?

刚开始我还是继续沿用 AvConf 里的 WorkerTaskQueue,不过在调研引入 HTTP server 的时候(「添加一个磁力链接和豆瓣电影链接」这个逻辑打算在投影端实现一个 HTTP server,在电脑上搜到磁力链接之后,通过 HTTP 请求提交),发现 Ktor(可以认为是 Kotlin 官方 HTTP 库)似乎已经支持了多平台支持,所以就想试试。不看不知道,好家伙,Kotlin/Native 在内存管理上有了一个大的更新,之前 Coroutine 的种种限制已经不复存在,所以单线程消息队列编程模式不再费劲了,那当然要试试啦!

不过这个信息是顺藤摸瓜看了好几篇文档才确认,最重要的还是官方这篇 Try the New Kotlin/Native Memory Manager Development Preview,里面也引用了好几篇 K/N 内存管理相关的文档。

单线程消息队列 2.0

启用新的 K/N Memory Manager 之后,可以直接在 common 代码里这样很简单地实现单线程消息队列编程模式:

val scope = CoroutineScope(newSingleThreadContext("OBWorker"))

scope.launch {
    // run on worker thread
}

withContext(Dispatchers.Main) {
    // run on main thread
}

上述 block 里,可以随意引用外面的变量,再也不用 freeze 了,也不用担心忘记 freeze 而 crash 了!

新的 K/N Memory Manager 采用的是 GC 方案,而且当前性能还欠佳,不过也不用看见 GC 就喷,安卓一直都是 GC 不也好好的,未来还是很可期的。

单元测试

在编写单元测试的过程中,我发现了 Kotlin 里很强大的 mock 框架:MockK,使用非常方便。当然,像 Coroutine、Ktor 这样成熟且官方的框架,本就有了很好的测试支持,基本参考官方的文档就行了:Testing Kotlin coroutines on AndroidKtor client testing

不过有三点值得分享一下。

一是在 Kotlin multiplatform 的项目里引入 MockK 时,需要在 commonTest 引入 "io.mockk:mockk-common:${Vers.mockk}",然后在 androidTest 引入 "io.mockk:mockk:${Vers.mockk}",这样可以在 commonTest 里编写共享代码的测例,并在 JVM 平台(也就是电脑上)运行测例。

二是我们的单线程消息队列 CoroutineScope 不能在被测试的代码内部构造,要通过参数传入,这样我们在测例里就可以注入测试的 dispatcher,便于在等待 coroutine 都执行完毕后再执行校验逻辑。

三是 Ktor 在发送 HTTP 请求时会切换到后台线程执行 coroutine,这样会导致测例执行的时候,我们的校验时机不好把控。我最先想到的也是 Ktor 是否支持设置 CoroutineContext/Scope,不过后来发现我们使用 MockEngine 的时候,它是支持设置 dispatcher 的,所以也就可以实现「等待 coroutine 都执行完毕后再执行校验逻辑」这个目的了。

展望

项目最新的完整代码我都已经发布到了 GitHub,感兴趣的朋友可以看看。不过目前在 iOS 上我碰到了 K/N 的一个 bug:在工作线程里做正则匹配的时候,有个栈溢出的 crash,已经在 Kotlin Slack 上提出来了,目前倒也不阻塞我。

Kotlin multiplatform 经过这一年多的发展,可以说有了很大的改进,iOS 库也支持 cocoapods 发布了,date/time 也有了多平台官方库,就连 SQL 也有了多平台库(而且就是老朋友 SQLDelight)

最后再分享一个 Kotlin multiplatform 的上手项目(最新定位是最佳实践):touchlab/KaMPKitTouchlab 的一个创始人我在 20 年 5 月份的时候还请教过 K/N 的一个偶现 crash 问题,刚开始大佬表示“嗯你遇到的问题很有趣,让我来瞧瞧”,不过后来他就再也不鸟我了,最后过了两个月,等 Kotlin 发布了新的版本,crash 的行为发生了一些变化,才发现是有几个地方忘记 freeze 了导致的。