ovCompose(online-video-compose)是腾讯大前端领域 TDF 端框架 Oteam 中,腾讯视频团队基于 Compose Multiplatform 生态推出的跨平台开发框架,旨在弥补 JetBrains Compose Multiplatform 不支持鸿蒙平台的遗憾与解决 iOS 平台原生 UI 混排受限的问题,便于业务构建全跨端 App 。同时腾讯视频深度参与 Oteam 并推出了 KuiklyBase,涵盖 Kotlin/Native 的鸿蒙适配、组件生态、鸿蒙编译、堆栈还原、工具链相关建设,助力业界 KMP 开发者提高鸿蒙适配效率。ovCompose&KuiklyBase 现已在 GitHub 开源,让我们一起深入实现细节。
随着纯血鸿蒙的推出,客户端跨平台需求被推到了前所未有的高度,单纯的UI跨端已无法满足业务诉求,构建Android/iOS/鸿蒙平台的全跨端APP能够最大幅度的降低业务开发成本,提升人效。并且行业内研发模式的逐步改进,单周发版已经成为常态,对于常规APP,动态化的诉求并不是很强。开发者普遍希望在保持原生优良性能的同时,使用行业通用的UI开发语言,从而最大程度降低学习成本。
Kotlin与 Compose是 Google官方推荐的 Android开发语言与 UI框架,也是深受开发者喜爱应用开发方案。与其他跨端方案相比,Kotlin Multiplatform还具备高性能,与原生交互更灵活等优点。因此腾讯视频选择了Compose Multiplatform作为全跨端APP的基础。当然,这套方案也存在不支持纯血鸿蒙、iOS平台混排能力受限、GC性能表现一般等一系列问题,使得落地的过程充满了挑战。经过不懈努力,上述问题均已得到妥善解决,现在我们希望将这些解决方案开源,期待与全行业一同推动Compose跨端生态走向成熟。
ovCompose已经在腾讯视频鸿蒙平台全面落地,成为鸿蒙平台首个全跨端APP。同时KuiklyBase基础能力已在腾讯视频、QQ浏览器、腾讯体育等10+款APP广泛落地。Android、iOS、鸿蒙三端一码的开发方式,使得业务的开发效率得到大幅度的提升。随着鸿蒙系统的发展,ovCompose和 KuiklyBase也会在未来进一步扩展到TV和PC端。
2.1 鸿蒙高性能
Kotlin鸿蒙适配有JS与Native两种技术方案可供选择,KuiklyBase最终选择了Native方案。因为KN相比JS有更快的执行速度,更好的三端一致性。
关于JS和KN的性能测试的数据如上图所示。我们对KN和Compose两者都进行了性能优化,在Compose"小球碰撞"Demo中。以30FPS为最低极限,经优化小球数量由600提升到1500(Android1600球),绘制性能提升150%。后续将开放更多优化策略。
相关资料如下:
JS和 KN性能测试详细数据:https://docs.qq.com/sheet/DQXB4YmxQaENSdkpD
ovCompose小球碰撞 Demo:
https://github.com/Tencent-TDS/ovCompose-sample
Kotlin官方的 Benchmark:
https://github.com/JetBrains/kotlin/tree/master/kotlin-native/performance/ring
2.2 鸿蒙三明治架构支持混排鸿蒙高性能
鸿蒙平台采用了Skia的渲染方案,能够100%支持Compose语法和渲染能力。Skia渲染使用XComponent组件作为画布,通过三明治镂空结构,很好的解决了与原生组件的混排问题,原生UI可以展示在Compose上层或下层,满足了绝大部分的业务需求。同时支持了粘贴按钮等安全组件的混排,使得 Compose无需申请权限也能使用系统能力。
2.3 三端高一致性
对于逻辑运行:由于在鸿蒙平台采取 Kotlin-Native 方案,解决了 Kotlin-JS 使用 TaskPool 时,Kotlin 语法无法约束跨线程访问的问题,保持了高度的三端一致性。
对于UI绘制:iOS、鸿蒙平台均采用Skia渲染,Android底层使用Skia渲染,应用层暴露了Paragraph/Canvas的绘制接口。所以基于Skia封装后的Skiko可以完美还原Android绘制效果,达到三端一致的效果。三平台均可以100%使用Compose的控件与绘制能力。
2.4 iOS多模态渲染解放混排能力三端高一致性
iOS端大量存量业务模块高度依赖 Compose与原生 UI的混合编排能力,其灵活混排的技术实现及与原生UI性能标准的精准对齐,是业务Compose化改造成功推进的核心前提。
Compose Multiplatform官方在 iOS端上使用 Skia+CAMetaLayer实现 UI的渲染能力,这种方案的好处是与其他端表现完全一致,缺点就是与原生UI的混排能力较弱,且内存占用较高,不适合多个Compose实例并存。因此我们必须考虑以下两种方案:
组件映射方案在组件层进行映射实现,是业内最常见的跨端UI框架设计方案,实现难度相对较低,但存在后期维护成本高,多端不一致等问题。指令映射在画布层进行映射实现,实现的逻辑层级更低也更加抽象,虽然开发难度相对较高,但却可以充分利用UIKit丰富的渲染能力对Compose的绘制效果实现较高的还原度。
因此我们最终采用了指令映射的自研实现方案解决了 Compose在 iOS上面临的诸多难题。这套方案也成功地在腾讯视频iOS端核心业务场景落地。事实上,业务团队甚至可以根据实际应用场景在基于UIKit实现的自研指令映射方案或官方的Skia渲染方案之间进行自由切换,并且可以在Runtime期共存。
关于UI的多端一致性,文本渲染较为复杂,我们采取Skia将文本渲染成图片,利用CALlayer进行展示的方案,保持了高度的一致性。
2.5 Kotlin Native内存优化
2.5.1 GC优化
GC抑制
当APP处于滑动等对帧率要求较高的场景,我们会短暂抑制GC,来换取更好的流畅度。
GC分段
不影响帧率情况下,进行更高频次的 GC,降低 PSS水位,通过分析 CMS(Concurrent Mark-Sweep)垃圾回收算法,发现其存在两次Stop-The-World (STW)暂停,并且第一次 STW时间较短,第二次 Sweep期间的 STW较长。利用 GC挂起的能力,我们在Vsync时进行GC挂起,在idle时进行GC恢复。具体效果如下图:
Sweep优化
Kotlin Native GC在Sweep阶段,会有大量的munmap系统调用,导致STW时间过长,从而影响主线程。为此,我们将munmap移出STW阶段,在STW阶段仅做Page收集。在Resume后再进行集中munmap。将第二次STW时间降低到1ms以内。
2.5.2 KN堆Dump优化
Kotlin Native(KN)支持生成堆内存转储文件,用于内存泄漏排查(类似 Android Profiler),但Dump过程需暂停所有KN线程,导致秒级界面冻结。针对不同平台特性我们采用了不同优化方案,从而达成线上可用的目标。
鸿蒙系统
基于Linux内核的fork()系统调用特性,采用「父进程无感知-子进程异步转储」方案实现零延迟内存快照。
iOS系统
针对iOS系统无法支持fork的限制,我们重新设计了堆内存分析流程,在保持性能的同时显著降低主线程阻塞时间。
堆冻结阶段:将堆内存数据保存到缓存文件,这里的堆内存是指KN堆用来分配对象内存的几种 Page类,粒度很大,一种 Page可能会有上千个对象,写文件时无需解析 Page内容,所以耗时很少且不会因为对象数量的增加而显著增加耗时。
线程恢复后:异步地从缓存文件中读取对象内容并写到Dump文件,由于每次从文件读取的只是对象大小数据,所以内存消耗很低。
优化后450MB堆内存转储耗时从2.8秒降低到410毫秒达到线上可用水平。该功能预计6月份上线。
方案对比:
维度 | 原方案 | fork子进程 | 堆文件异步分析 |
---|---|---|---|
挂起耗时 | 秒级别 | 纳秒级别 | 毫秒级别 |
内存 | 低 | 低 | 低 |
磁盘 | dump文件 | dump文件 | dump+cache文件 |
适用平台 | 全平台 | Linux/鸿蒙 | iOS/非fork环境 |
3.1 KN鸿蒙平台适配
kotlin 1.9使用的LLVM 11,kotlin 2.1升级到LLVM 16,但是鸿蒙平台能够支持的版本在LLVM12~15,苹果和鸿蒙都是基于公共版本的LLVM进行修改,增加了自己的特性优化,苹果相对好的点在于公共版本的LLVM中包含有苹果的target,所以鸿蒙版本的LLVM既可以支持iOS,又可以支持鸿蒙平台。(KukilyBase-Kotlin当前基于2.0.21进行鸿蒙适配)
3.2 KN性能优化
完成适配后,我们发现卡顿情况非常严重,从而进一步对Kotlin-Native性能进行评估,我们采用了官方Benchmark进行对比,测试发现鸿蒙耗时是iOS相同性能机器的2.48倍。
我们需要针对鸿蒙平台进行一系列的优化,经过初步分析,我们也规划了性能优化的初步优化思路。
3.2.1 内联优化
我们分别对比了相同benchmark生成的Kotlin IR、LLVM IR文件。发现LLVM IR在内敛上更加充分,特别是对于关键函数,例如EnterFrame等,反观鸿蒙平台此类优化更少有。
尝试添加always inline后,发现程序性能得到了较为显著的影响。但相对iOS仍然有一定差距。通过分析LLVM的内联pass发现,在处理EnterFame等函数时,会对比cpu feature的兼容性,Kotlin和框架内部C++代码在生成LLVM的函数时,他们各自携带的cpu feature不一致,导致无法进行内联。配置正确的属性后,此问题得到修复。
3.2.2 ThreadLocal引发的性能低
通过对 Benchmark中耗时超 iOS的 case进行深度分析,最终发现如下高频堆栈。线程私有数据的性能测试结果表明Ohos耗时波动较大。Ohos耗时是iOS的2-3倍。(展示0ns是由于初期鸿蒙trace工具不完善导致,现已修复)
由于Kotlin-Native在内存分配时都依赖ThreadLocal来访问线程独立的Page,固访问频率极高,导致性能低下。分析发现鸿蒙平台默认采用了软件模拟的 thread_local。所以我们在编译时通过参数强制使用硬件thread_local,整体性能提升了30%。
3.2.3 协程性能优化
将JetBrains的Compose成功适配到Ohos后,长列表的滑动过程中频繁出现卡顿现象。trace分析发现异常的处理花费了大量的时间。
经技术架构分析,Compose Multiplatform框架的协程调度机制深度依赖异常处理模型实现任务恢复与取消控制。其底层实现中,KN运行时将异常处理桥接至C++异常体系,该设计在运行时会产生显著性能损耗:当异常触发时,系统需沿调用栈进行逐级回溯以定位匹配的异常捕获点,其时间复杂度与调用栈深度呈正相关。更值得注意的是,该过程伴随大量C++异常对象的动态构建与析构操作,频繁的内存分配与释放行为进一步加剧了执行时延,导致关键路径上的协程调度效率受限于异常处理机制的性能瓶颈。
同时鸿蒙系统 libhilog.so捕获了抛出的异常进行处理,造成了大量延迟,与鸿蒙专家沟通后得到妥善优化。最终长列表在滑动场景能够稳定在 120Hz,处理方式如下:
3.2.4 调试性能优化
使用JetBrains的Kotlin Native debuging脚本后,调试断点及打印变量耗时远超Native。通过trace分析发现其KDS与LLDB交互极为原始和简单。
经技术架构分析与处理,在KDS与LLDB上运用流程合并、复用、缓存、预加载潜在下一跳、局部调试的可容错优化等手段提高其通信和处理效率。整体性能视实际情况提升数倍至几十倍(提升幅度随调试栈的变量加密加深等因素影响),近似Native。
3.3 鸿蒙绘制不同步问题解决
由于两种组件属于独立的绘制层,在鸿蒙系统中存在不同步的问题。整体效果如图,Compose的列表混排ArkUI的元素进行滚动,两个同步向上进行运动,由于不同步,UI衔接处会展现出空白区域,出现割裂的现象。
核心问题是鸿蒙采用的是集中渲染架构,XComponent的独立绘制模式与ArkUI的绘制发生在不同的进程,无法保证完全不同。所以我们采用XComponent的Texture模式,将内容绘制到FBO中,由FBO参与原有的ArkUI的绘制节奏,来保证完全的同步。
3.4 iOS多模态渲染
在基于UIKit进行渲染的基础思路上,我们也发现了如CALlayer重叠、未正确放置、无法复用等问题。对于Android来说,其是独立绘制架构,每个进程自己完成内容的绘制,所以画布是一整块,内容都绘制在其中,通过Skia的 PictureRecorder命令录制功能进行命令的快速回放。但这种模式在 iOS集中渲染架构上就不太适用了,需要有一个工具来进行差量处理绘制命令。所以我们设计了基于iOS的PictureRecorder局部更新架构。
在PictureRecorder中,我们对绘制命令进行差量,只更新变化的部分,从而提升绘制效率。PictureRecorder核心就是我们通过hash来判断绘制指令是否发生变化,常规的这种方式能够提升绘制效率,但当页面无比复杂时,hash计算偶尔也会变成一种负担。
PictureRecorder进行了进一步升级
我们优化的核心思路是,通过增量hash来减少hash的计算量。每一个draw函数执行的时候,都会对将当前的hash和指令id进行一次合并。并计算出最终的hash。这个hash记录了一次完整的使用。增量hash的目的是减少diff操作,这种方式可以有效的减少,两次指令相同的比较。在压力测试中还发现OC对象的创建和释放耗时也会被放大。这种情况在腾讯视频复杂页面回迁的过程中尤其明显,因此,这里还将原先由OC对象代表的指令,改为了非常简单的C结构体。之前的OC闭包也去掉了。
3.5 与 KuiklyUI的差异
跨端框架自渲染与原生渲染在性能表现与多端适配层面各具优势。为满足业务场景的差异化需求,腾讯大前端Oteam同时进行两个方案探索。
目前ovCompose和KuiklyBase已开源,包含5个仓库,包含了。仓库Group地址为:https://github.com/Tencent-TDS
仓库名 | 说明 |
---|---|
ovCompose-sample | 展示 ovCompose与 KuiklyBase的功能。 |
ovCompose-multiplatform-core | 基于JetBrains的 compose-multiplatform-core定制,实现鸿蒙适配 |
KuiklyBase-kotlin | 基于JetBrains的kotlin-multiplatform定制,适配鸿蒙平台并进行性能优化 |
KuiklyBase-components | 封装常用的跨端组件,涵盖资源管理、跨语言通信、网络请求和图形动画等 |
KuiklyBase-platform | 基于官方组件适配鸿蒙平台,并对部分功能进行性能优化,提升开发体验 |
随着这几年的快速发展,KMM生态得到了长足的发展,Kotlin-Native的执行性能在很多方面已经超越了Kotlin-JVM,但目前Compose Multiplatform跨平台技术还没有达到成熟的状态(特别是GC),ovCompose& KuiklyBase将持续优化,为开发者带来体验更好、性能更强的跨端开发体验。以下是我们重点优化的方向:
-End-
原创作者|何加淼