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

相关推荐
光芒Shine2 小时前
【HarmonyOS-北向开发(软件)】
harmonyos
猫林老师5 小时前
Flutter for HarmonyOS开发指南(四):国际化与本地化深度实践
flutter·华为·harmonyos
猫林老师11 小时前
Flutter for HarmonyOS 开发指南(一):环境搭建与项目创建
flutter·华为·harmonyos
爱笑的眼睛1113 小时前
HarmonyOS通知消息分类管理的深度实践与架构解析
华为·harmonyos
爱笑的眼睛1114 小时前
HarmonyOS Menu组件深度自定义:突破默认样式的创新实践
华为·harmonyos
编码追梦人17 小时前
仓颉语言:全栈开发新利器,从服务端到鸿蒙的深度解析与实践
jvm·华为·harmonyos
爱笑的眼睛1118 小时前
HarmonyOS输入法框架(IMF)深度解析:构建跨设备智能输入体验
华为·harmonyos
特立独行的猫a18 小时前
鸿蒙应用状态管理新方案:AppStorageV2与PersistenceV2深度详解
华为·harmonyos·状态管理·appstoragev2·persistencev2
奔跑的露西ly18 小时前
【HarmonyOS NEXT】Navigation路由导航
华为·harmonyos