(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,序列化卡顿优化,鸿蒙开发套件,架构设计思路等等,欢迎关注!

相关推荐
李蚊子29 分钟前
鸿蒙应用开发实践与上架复盘
harmonyos
2501_919749031 小时前
华为鸿蒙训练口才APP—小羊口才
华为·harmonyos·鸿蒙
大锅盖12 小时前
HarmonyOS 摄像头焦点管理的完整工程实现
华为·harmonyos
woshihuanglaoshi4 小时前
鸿蒙老旧项目升级改造高级:API废弃迁移/ArkTS语法升级/架构重构策略/增量迁移/风险管控方案
学习·华为·重构·架构·harmonyos·鸿蒙
星空真迷人1 天前
嵌入式鸿蒙并非精简版,核心究竟是什么?
stm32·单片机·嵌入式硬件·物联网·华为·harmonyos·iot
OH_TPC1 天前
【鸿蒙优选三方库】@ohos/ijkplayer:在鸿蒙上像 B 站一样流畅播视频
华为·音视频·harmonyos·鸿蒙
2501_919749031 天前
华为鸿蒙经期记录APP—小羊月经
华为·harmonyos·鸿蒙
黑鲨吃西瓜2 天前
鸿蒙通用模块
harmonyos·鸿蒙
腾科IT教育2 天前
HarmonyOS开发|ArkTS UI颜色API通用规则
ui·华为·harmonyos·harmonyos开发·鸿蒙应用开发工程师
风华圆舞2 天前
HarmonyOS 自定义绘制实战 —— 用 ArkGraphics2D 画一个会卷曲翻动的页面网格
harmonyos·arkts·drawing·drawvertices·翻页卷曲·有限差分法线