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

相关推荐
SummerKaze1 天前
为鸿蒙开发者写一个 nvm:hmvm 的设计与实现
harmonyos
在人间耕耘3 天前
HarmonyOS Vision Kit 视觉AI实战:把官方 Demo 改造成一套能长期复用的组件库
人工智能·深度学习·harmonyos
王码码20353 天前
Flutter for OpenHarmony:socket_io_client 实时通信的事实标准(Node.js 后端的最佳拍档) 深度解析与鸿蒙适配指南
android·flutter·ui·华为·node.js·harmonyos
HarmonyOS_SDK3 天前
【FAQ】HarmonyOS SDK 闭源开放能力 — Ads Kit
harmonyos
Swift社区3 天前
如何利用 ArkUI 框架优化鸿蒙应用的渲染性能
华为·harmonyos
特立独行的猫a3 天前
uni-app x跨平台开发实战:开发鸿蒙HarmonyOS影视票房榜组件完整实现过程
华为·uni-app·harmonyos·轮播图·uniapp-x
盐焗西兰花3 天前
鸿蒙学习实战之路-STG系列(5/11)-守护策略管理-添加与修改策略
服务器·学习·harmonyos
盐焗西兰花3 天前
鸿蒙学习实战之路-STG系列(4/11)-应用选择页功能详解
服务器·学习·harmonyos
lbb 小魔仙3 天前
鸿蒙跨平台项目实战篇03:React Native Bundle增量更新详解
react native·react.js·harmonyos
特立独行的猫a4 天前
uni-app x跨平台开发实战:开发鸿蒙HarmonyOS滚动卡片组件,scroll-view无法滚动踩坑全记录
华为·uni-app·harmonyos·uniapp-x