(3)Kotlin/Js For Harmony——解决官方库序列化卡顿

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对齐的。

最后,如果大家自己实现,有一些建议:

  1. 在生成序列化辅助方法的过程中,需要注意一些特殊类型,比如List,Map,enum,Long。因为List,Map 还包含子对象,所以最好以递归的方式实现代码生成的逻辑。
  2. 反序列化的过程中,可以提供一个工具方法,生成的代码直接调用这个工具方法去反序列化即可,这样写逻辑比较简单。
  3. 在实现的过程中,可以加些自定义注解,用于修饰函数,该函数会替换ksp默认生成的序列化/反序列化方法。这样你的序列化库会非常灵活。

按照这套思路实现,实测在鸿蒙上大json的序列化速度得到了60倍的提升,并且json 越长,提升越明显,看一下统计数据,红线是官方库,黄线是我们自己实现的库:


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

相关推荐
lbb 小魔仙6 小时前
【HarmonyOS实战】OpenHarmony + RN:自定义 useValidator 表单验证
华为·harmonyos
一起养小猫8 小时前
Flutter for OpenHarmony 实战:扫雷游戏完整开发指南
flutter·harmonyos
小哥Mark10 小时前
Flutter开发鸿蒙年味 + 实用实战应用|绿色烟花:电子烟花 + 手持烟花
flutter·华为·harmonyos
前端不太难11 小时前
HarmonyOS 游戏里,Ability 是如何被重建的
游戏·状态模式·harmonyos
lbb 小魔仙12 小时前
【HarmonyOS实战】React Native 鸿蒙版实战:Calendar 日历组件完全指南
react native·react.js·harmonyos
一只大侠的侠12 小时前
Flutter开源鸿蒙跨平台训练营 Day 3
flutter·开源·harmonyos
盐焗西兰花12 小时前
鸿蒙学习实战之路-Reader Kit自定义字体最佳实践
学习·华为·harmonyos
_waylau12 小时前
鸿蒙架构师修炼之道-架构师的职责是什么?
开发语言·华为·harmonyos·鸿蒙
一只大侠的侠13 小时前
【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day 2 鸿蒙跨平台开发环境搭建与工程实践
flutter·开源·harmonyos
王码码203516 小时前
Flutter for OpenHarmony 实战之基础组件:第三十一篇 Chip 系列组件 — 灵活的标签化交互
android·flutter·交互·harmonyos