前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >KMM 求生日记二:Kotlin/Native 被踩中的坑

KMM 求生日记二:Kotlin/Native 被踩中的坑

作者头像
bennyhuo
发布2021-03-18 15:20:04
2.3K0
发布2021-03-18 15:20:04
举报
文章被收录于专栏:BennyhuoBennyhuo

最近几周团队的 KMM 进度推进了不少,已经陆续把几个小业务需求迁移到了 KMM。

其实万事开头难,最初的时候许多公共团队基础类库都没有桥接到 KMM,导致好像啥业务都没法着手开始。确定了临时的方案之后,经过前面一周的集中攻克,目前公共的团队最常用的诸如:AB 实验、增量数据、网络、日期、本地存储等等核心 API 都已经桥接完成。

目前基本的开发方式是,主要由我来编写 KMM 工程的代码,包括需要主工程实现的接口、以及通过桥接过来的对象实现的各种基础 API,最后是基于这些 API 编写的真正的业务逻辑。然后我在 Android 的主工程中编写这些桥接接口的实现,以及对 KMM 中业务逻辑的调用。在 Android 上完成基本的测试,能跑通之后,由我的一位同事(也是小组 leader)在 iOS 主工程中编写类似我在 Android 主工程中编写的代码(Objective-C)。

其实在 Android 上基本没什么问题,因为本质就是用 Kotlin 写了些代码,打成 aar 给主工程调用而已,所以主要的问题还是出在 iOS(Kotlin/Native)上。

一. Kotlin 类的根级超类与 Objective-C 的根级超类不兼容

Kotlin 中有一个类 Any,它是所有类的根级超类。Java 所有类的根级父类是 Object,但是在 Kotlin/JVM 中,这两者是统一的,也就是说如果一段 Java 代码接收的参数类型是 Object,那我们仍然可以将任意一个 Kotlin 对象作为参数传入。

但是情况到了 Kotlin/Native 中则完全不同。如果我们打开一个依赖了由 Kotlin/Native 编译出来的 Framework 的 XCode 工程,在该工程中我们会发现所有的 Kotlin 类都继承自一个叫做 KotlinBase 的类,声明如下:

代码语言:javascript
复制
open class KotlinBase : NSObject {
    open class func initialize()
}

这是一段 Swift 代码,只要继续浏览这个声明文件我们会发现我们所有的 Kotlin 类都继承自 KotlinBase。

但是到了 Kotlin 工程中情况就变的完全不同了,所有 Kotlin 类继承自 Any,而 Any 和 NSObject 之间没有任何类型关系。

上述差异导致的最严重问题就是 Kotlin/Native 类在 Kotlin 工程中拿不到 class 对象。在 Java 中所有类都有类型为 Class<?> 的 class 对象,通过类名或该类的对象都可以直接获取。在 Kotlin/JVM 中,Kotlin 有自己的 KClass<*> 类型,它与 Java 的 Class<?> 类型不同,但是我们可以用 Any::class.java 的方式拿到一个 Kotlin 类的 Java class 对象。而在 Kotlin/Native 中,KClass 无法获取一个类的 Objective-C 的 class 对象,这最直接的结果就是许多现有的 Objectice-C 库,可能含有需要传入一个 class 对象的 API,通常的左右是来生成一个对象(和 Java 中使用 class 的方式相似),那么这样的 API 可能对 Kotlin 类不兼容。

但奇怪的是,在 Kotlin 工程中如果直接声明一个类继承自 NSObject,可以用 class() 函数来获取自身的 class 对象,但普通的 Kotlin 类则没有这个函数。

二.object 定义的作用域内如果存在可变状态,则必须添加 @ThreadLocal 注解

如果我们用 object 定义了一个单例(其实更多的时候我们只是想要一个 name space),其内部存在可变状态,一旦对其进行更改(无论是否在别的线程进行),都会抛出 InvalidMutabilityException 异常。例如考虑如下代码:

代码语言:javascript
复制
object MyObject {
    var index = 0
}

即使不运行,编译器也会抛出警告:“Variable in singleton without @ThreadLocal can't be changed after initialization”。如果我们在运行中对其进行修改,会直接抛出 InvalidMutabilityException 异常并 crash。由于警告的存在,上面这段代码很容易让开发者发现问题。但是再考虑一下下面的代码:

代码语言:javascript
复制
object MyObject {
    val hashMap = HashMap<String, String>()
}

由于 hashMap 是用 val 定义的变量,所以编译器不会抛出警告,但一旦我们对 hashMap 进行 put 等操作,程序一样会因为 InvalidMutabilityException 而 crash。

以上说明在 Kotlin/Native 的开发中还有一条不成文的规定:除非你的 object 作用域内仅存在常量、纯函数,否则一定要加上 @ThreadLocal 注解。但你可能会说,加了 @ThreadLocal 注解全局可变状态该怎么定义?那我只能告诉你别想了,Kotlin/Native 的世界里不存在这东西。

三. iOS 平台的 size 增长较大

Android 平台以 aar 的形式集成,许多依赖的 Kotlin 基础库,例如 kotlinx.coroutines 以及 kotlinx.serialization 等等都没有打进这个 aar 里,再加上编译产物又是字节码,总 size 增长只有 0.03 MB。但是 iOS 有所不同,编译产物是二进制码,再加上整个 Kotlin/Native 的基础库、Runtime 等等通通打进了这个 Framework,总 size 增长为 1.5 MB,当然后续再持续集成业务代码的话,增长幅度不会再这么惊人。

结语

KMM 代码发布上线在即,如果它能在线上稳定跑一到两个版本(主要担心的就是 iOS 平台),就至少可以说明 Kotlin/Native 的编译器、 runtime 以及标准库没有太大问题,当然 runtime 的坑之前不是没遇到过,例如 Kotlin/Native 没有 JVM 上的虚方法调用动态分派。如果能证明语言层面上问题,后续 Model 层的业务逻辑就可以大规模迁移到 KMM。

后面我们可能会把精力花在研究一下 cinterop 这个工具以及 iOS 的构建系统上。如果能用 cinterop 搞定对已有的 iOS Framework 或 .a 文件的依赖,我们可以基于许多已有的 Objective-C 库和 Java 库封装出许多实用的 KMM 库,而暂时不必用 Kotlin 重写许多基础组件代码。

上面提到的东西够我们做一阵子了,如果再往后,就可以考虑完善一些平台统一的上层建筑,例如一些和 UI 生命周期绑定的 VM 层框架,像 Jetpack 的 ViewModel 和 LiveData 这种,可能要在双平台生命周期对齐封装方面下一番功夫。

最近 Compose-jb 动态频频,Skiko 这个库更新的也很频繁(Compose-jb 的底层依赖),社区对于 Compose-jb 支持 Native 平台呼声很高,关于 iOS 平台的相关代码也已经有社区大佬开始提交,长远来看我觉得可以期待一下。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
持续集成
CODING 持续集成(CODING Continuous Integration,CODING-CI)全面兼容 Jenkins 的持续集成服务,支持 Java、Python、NodeJS 等所有主流语言,并且支持 Docker 镜像的构建。图形化编排,高配集群多 Job 并行构建全面提速您的构建任务。支持主流的 Git 代码仓库,包括 CODING 代码托管、GitHub、GitLab 等。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档