KMP 的 序列化库 kotlinx.serialization 在Js 上非常慢,鸿蒙上目前在子线程处理限制颇多,导致复用Android的代码基本上都是在主线层跑。 这就导致了序列化慢的问题尤为突出,一个大json会耗时几百上千毫秒,严重影响主线程流畅度。
kotlinx.serialization 为什么慢
kotlinx.serialization是被编译成Js代码拿到鸿蒙去使用的,在他序列化过程中,涉及HashMap(Kotlin/Js 编译成的HashMap很慢,后面的文章会介绍如何优化),string to int 等等操作,这些操作在纯Js 层做,效率极低。 而反观 JSON.parse/stringify, 这个Js 提供的默认序列化/反序列工具,已经在引擎层充分优化,速度极快。
所以,要解决序列化卡顿,核心思路是要利用JSON.parse/stringify来代替kotlinx.serialization 的序列化流程。
HarmonySerialization
为了解决上述问题,我们需要自己处理序列化,将逻辑桥接到JSON.parse/stringify。 整体思路如下:利用ksp在编译期生成序列化、反序列化辅助工具方法,运行时利用JSON.parse得到json对象,再调用辅助方法完成序列化,反序列化同理。 以class Student为例:
less
@Serializable
data class Student(
@SerialName("name_cn")
val nameCN: String,
)
ksp 在编译期扫描收集所有被@Serializable修饰的class, 针对Student,会生成如下代码
kotlin
@JsExport
class StudentJsonHelper {
companion object {
fun fromJson(json: Json?): Student? {
val result = Student(nameCN = (json?.get("name_cn") as? String)!!)
}
fun toJson(obj: Student): Any {
val jsObj = js("{}")
jsObj.`name_cn` = obj.nameCN
}
}
}
val fromJsonRegistry: MutableMap<String, (Json) -> Any?> = HarmonyMutableMap()// 后续文章会提到,是一个针对鸿蒙平台优化的HashMap
val toJsonRegistry: MutableMap<String, (Any) -> Any> = HarmonyMutableMap()// 后续文章会提到,是一个针对鸿蒙平台优化的HashMap
// 将生成的方法注册到Map中
fromJsonRegistry[Student::class.js.name] = {json -> StudentJsonHelper.fromJson(json)}
toJsonRegistry[Student::class.js.name] = {obj -> StudentJsonHelper.toJson(obj)}
在运行时,如果我们要序列化/饭序列化Student, 可以这样做:
ini
//序列化
val jsonString = "{ "name_cn": "xxx" }"
val studentObj = fromJsonRegistry["Student"]!.invoke(JSON.parse(jsonString))
//反序列化
val studentObj = Student(nameCN = "yyy")
val jsonObj = toJsonRegistry["Student"]!.invoke(studentObj)
整体思路就是上述这样,其中有个小坑需要注意一下: 不知道细心的你有没有发现,Student 的nameCN是没有默认值的,所以我们在生成序列化方法的时候 (json?.get("name_cn") as? String)!! 用了非空断言。但是如果nameCN 有默认值呢,即class Student 长这样:
less
@Serializable
data class Student(
@SerialName("name_cn")
val nameCN: String = "", // 有默认值
val age: Int, // 没有默认值
)
可以看到nameCN 默认值是一个空字符串。我们该如何拿到默认值呢? 当遇到这种情况,我们可以先构造一个对象,用来获取默认值,这种做法虽然会额外创造一个对象,但是胜在复杂度低,所以整体来说是最好的方法。我们看一下兼容默认值的fromJson函数是什么样:
kotlin
fun fromJson(json: Json?): Student? {
//构造一个提供默认值的对象
val defaultValueProvder = Student(age = (json?.get("age") as? Int)!!)
val result = Student(
nameCN = (json?.get("name_cn") as? String) ?: defaultValueProvder.nameCN,
age = (json?.get("age") as? Int)!!
)
}
注意看我在构造defaultValueProvder 的时候依然用了非空断言,是因为age没有默认值,所以构造对象时age一定不为空,这和语义相符,没什么问题。 还有一个小细节,age 并没有用 @SerialName 修饰,遇到没有@SerialName修饰的变量,默认以他的变量名作为json中的键去取值,这一行为也是和kotlinx.serialization对齐的。
最后,如果大家自己实现,有一些建议:
- 在生成序列化辅助方法的过程中,需要注意一些特殊类型,比如List,Map,enum,Long。因为List,Map 还包含子对象,所以最好以递归的方式实现代码生成的逻辑。
- 反序列化的过程中,可以提供一个工具方法,生成的代码直接调用这个工具方法去反序列化即可,这样写逻辑比较简单。
- 在实现的过程中,可以加些自定义注解,用于修饰函数,该函数会替换ksp默认生成的序列化/反序列化方法。这样你的序列化库会非常灵活。
按照这套思路实现,实测在鸿蒙上大json的序列化速度得到了60倍的提升,并且json 越长,提升越明显,看一下统计数据,红线是官方库,黄线是我们自己实现的库:

关于「解决官方库序列化卡顿」的介绍就告一段落了,如果大家在使用过程中有任何问题,欢迎留言讨论。 Android工程师的kmp(kotlin/js) for harmony开发指南 这一系列文章旨在系统性的提供一套完整的Kotlin/Js For Harmony的解决方案。后续系列文章会介绍如何复用ViewModel,序列化卡顿优化,鸿蒙开发套件,架构设计思路等等,欢迎关注!