随着互联网技术的快速发展,大前端领域正经历着前所未有的变革。从传统的 Web 开发到移动应用、小程序、IoT、乃至新兴的 AR/VR,大前端技术不仅需要适应越来越多样化的需求场景,还面临着如何更高效地利用现有资源、提升用户体验等挑战。与此同时,AI 等新技术的发展为解决这些问题提供了新的思路和工具。在即将于 4 月 10 日召开的 QCon 全球软件开发大会(北京站),我们策划了「越挫越勇的大前端」专题,将深入探讨大前端技术在当前及未来一段时间内的演进趋势及其实践案例。欢迎围观:摘要:随着互联网技术的快速发展,大前端领域正经历着前所未有的变革。从传统的 Web 开发到移动应用、小程序、IoT、乃至新兴的 AR/VR,大前端技术不仅需要适应越来越多样化的需求场景,还面临着如何更高效地利用现有资源、提升用户体验等挑战。与此同时,AI 等新技术的
性能和体验在 iOS / Android 双端场景下已经是一个较为成熟的,但随着鸿蒙 OS 的发展,端侧开发者需要更多的关注多端场景的差异性。在 InfoQ 举办的 QCon 全球软件开发大会(上海站)上小红书鸿蒙工程师王劲鹏为我们带来了精彩专题演讲“小红书鸿蒙 OS 下的性能优化探索与实践”,分享议题将以独特于双端的视角,分享小红书在鸿蒙 OS 上的性能创新实践。
内容亮点:
详细了解小红书在鸿蒙 OS 上的性能优化实践案例;
对比 Android/iOS 端的类同性能优化场景,以前端视角展示鸿蒙 OS 上特有的能力上有何不同。
以下是演讲实录(经 InfoQ 进行不改变原意的编辑整理)。
我分享的主题是小红书在鸿蒙平台上的工程实践,主要聚焦于性能优化和探索。首先,我先介绍一下自己的背景。我之前一直从事大前端领域的工作,主要专注于跨端和容器化方案。我也曾手写过一个跨端框架,名为 Doric,它可以对标 React Native、Vue Native 和 Flutter 等。Doric 框架在落地时表现良好,还支持了一些自研的 3D 引擎方案。除此之外,我还有播放器内核研发经验,以及大前端常规体系建设和 CI/CD 流水线的工程经验。未来,我将持续关注大前端的演进,尤其是鸿蒙这样的多端和跨端平台。
从 2023 年开始,鸿蒙的优势愈发明显,已经成为可与 iOS、安卓媲美的第三大移动操作系统。从一些抖音视频中也可以看出,鸿蒙在流畅性方面甚至在某些层面上超过了 iOS。
今天的演讲内容分为四个部分。第一部分是介绍整个历程和背景;第二部分是介绍鸿蒙 OS 的相关能力和小红书在该平台上的优化实践;第三部分是通过鸿蒙 OS 提供的性能验证工具,展示小红书在鸿蒙平台上的性能优化验证方法、优化后的性能提升以及具体的收益和结果;最后一部分是总结和展望。
历程和背景 小红书迭代历程从 2023 年年中开始,鸿蒙的“千帆计划”正式启动,并很快升级为“鸿飞计划”。小红书作为 7 家头部合作商之一,率先支持了鸿蒙,并于 2023 年 11 月中旬上线了一个基础版的 beta 版本 APP。这个版本主要包含笔记浏览和视频笔记浏览两大功能,以及一些简单的个人设置。当时,小红书的动作非常迅速,可以说是头部应用厂商中对华为支持最为积极的品牌之一。
在整个鸿飞计划中,我们规划了三个核心里程碑:除了 2023 年 11 月的 beta 版本外,还包括 2024 年 6 月的 HDC 版本和 2024 年 9 月的商用版本。HDC 版本主要是针对华为正式宣发鸿蒙 3(HarmonyOS Next)开发者测试的情况。在 HDC 版本中,我们上线了许多小红书特有的存量功能,包括视频拍摄、图文拍摄以及多设备协同等创新特性。而到了 2024 年 9 月的商用版本交付时,小红书的核心功能已经基本与主端对齐。考虑到鸿蒙的开发周期仅有一年,小红书的鸿蒙 APP 在这一年中要对齐开发了十年甚至十几年的安卓和 iOS 版本,难度和压力都非常巨大。
到 2024 年 9 月,除了对齐双端的所有功能外,我们还开发了许多其他功能,包括华为支持的创新特性,例如智能拖拽——用户可以将图片拖拽到中转站或小艺等场景。此外,商用版本还支持了用户呼声较高的 HDR 或 Moonlight Photo 拍摄能力。
纯血鸿蒙与安卓的区别我从几个维度来对比一下纯血鸿蒙和安卓 OS 的主要区别。
内核架构 纯血鸿蒙的本质是微内核,而安卓是基于 Linux 宏内核。微内核只提供基础的内存和文件管理能力,驱动和其他系统能力都在 OS 之外。这样做的好处是系统稳定性极高,即使应用崩溃,也不会导致整个系统崩溃(system crash)。而在 Linux 宏内核中,应用的不当行为可能会直接导致系统崩溃。
多设备适配 鸿蒙目前支持多种设备类型,包括 Mate 60 Pro 这样的直板手机、Mate X5 或非凡大师 XT 这样的双折叠和三折叠手机、平板电脑、车机,甚至华为正在研发的鸿蒙 PC。鸿蒙真正实现了类似 iOS 的多端整合能力,通过一套代码实现多端部署。其工程体系和架构支持单 HAP(Harmony Ability Package)多 HSP(Harmony Service Package)模块,指令集适配了 ARM64 等多种架构,开发者只需根据设备尺寸适配 UI 展示即可。例如,在 2024 年 9 月 的华为全场景设备发布会上,余承东展示了小红书在从直板机到双折叠、三折叠设备上的适配能力,完全实现了响应式编程,不同设备形态下有不同的浏览体验。
开发工具和编程模型 鸿蒙的开发工具和编程模型与安卓差异较大。鸿蒙更类似于 Flutter 的嵌套型容器布局,而不是安卓那种面向对象的开发方式。在语言层面,鸿蒙完全封装了底层逻辑,采用类似前端 Flux 单向数据流模式,通过数据变更驱动 UI 刷新。这种模式类似于前端 Redux 或 MobX 框架中的 state 管理 。
从 2024 年 10 月 8 日公测开始,鸿蒙的应用生态正在逐渐繁荣。不过,目前像微信这样的应用还处于抢先体验阶段。相比之下,安卓的生态已经相对成熟。鸿蒙的最终目标是打造全场景智能设备生态,涵盖所有终端设备,以及基于 OpenHarmony 内核开发的物联网终端。它还支持多种芯片体系,例如瑞芯微 RK3568 等。
小红书鸿蒙应用架构层级小红书经过一年的迭代,其整体应用架构已经基本成熟。目前,整体代码量接近 200 万行,达到了一个较高的复杂度。在一般成熟的 APP 架构中,通常会包含一些基础底层能力,例如网络、磁盘存储、埋点体系、APM(应用性能管理)系统,以及一些通用组件和能力。对于鸿蒙平台,小红书还具备一些特殊的公共通用能力。
我们开发了一个“一多框架”,这是一个支持一套代码多端部署的具体框架体系。通过这个框架,我们实现了多设备的断点控制功能。用户可以根据设备的尺寸和类型进行适配,因为华为设备支持多端投屏。例如,用户可以在手机上浏览小红书,然后将内容投屏到车机上。比如用户购买了一辆问界汽车,可以在车内通过车机继续浏览手机上的小红书内容,这种场景在驾驶时尤其有用。
除了底层框架,对于上层业务,小红书还有一套自研的组件库方案,这套组件库承载了上层业务的多种功能,包括图文笔记、视频笔记浏览,以及一些 Hybrid 容器能力。小红书本质上在跨端开发中仍然使用了 React Native(RN)和类 Web 技术。RN 引擎由华为内部合作提供,采用了自研的 ohos 方案,用于解决 React Native 的 bundle 和 JS 加载以及渲染问题。此外,还包括产品定制层,这里涵盖了所有相关的设备适配内容。
目前,安卓和 iOS 在性能优化方面已经相当成熟,包括如何分析性能热点问题、有哪些工具以及最佳实践等。然而,对于鸿蒙来说,它是一个全新的系统。直到 2024 年年中,鸿蒙的稳定性和流畅性都还存在一些问题。这里重点讲述小红书在 2024 年与华为一起进行了哪些实践,以提升应用的性能和用户体验。
我们定义了一个性能指标场景。这个指标体系是小红书与华为共同探讨的结果,因为华为有一个性能工厂,它对每个应用的评级都有一个 S 标标准。小红书与华为一起确定了针对小红书场景需要观测的具体指标。性能优化的核心是慢函数指标,它主要包含两部分:过程时长和应用体验的流畅性。
过程时长主要包含以下三点:
冷启动时长 :这是用户最关心的指标之一,即从点击应用图标到应用完成动画并展示第一帧的时间。对于多数应用,首页通常有缓存机制。例如,小红书会缓存用户上次刷新的笔记,淘宝会缓存用户上次浏览的商品内容。
场景完成时长 :指完成某个特定场景所需的时间。
应用响应时长 :指用户操作界面后,界面真正发生变化的时间,即响应时延。
流畅性方面,最基础的观测指标是平均 FPS(帧率),包括丢帧数、最大连续丢帧数、丢帧卡顿次数以及卡顿率。卡顿率可以通过量化计算得出:当一个场景中出现丢帧时,丢帧的时长与场景总时长的比值即为卡顿率,它是一个小于 1 的百分比数值。
OS 能力 优化实践首先,针对 IO 场景,我们进行了相应的优化。鸿蒙 OS 的系统能力主要分为以下三个方面:
并行化能力 鸿蒙 OS 提供了两种并行化能力:Worker 和 TaskPool。Worker 类似于传统的线程模型,每个 Worker 都有自己的内存空间和执行单元,支持通过消息(message)进行通信。TaskPool 则类似于协程或线程池,能够动态管理线程数量,支持标记为 @concurrent 的函数直接在任务池中调度和运行。这两种机制都支持线程间隔离,内存不共享。
多线程通信和数据传输 在多线程通信方面,鸿蒙 OS 支持序列化数据传输和基于消息(message)的通信机制。此外,还引入了事件发射器(Emitter)用于系统事件的发布和订阅。这种机制允许线程间通过消息传递来实现复杂的交互逻辑。
同步转异步机制 鸿蒙 OS 支持基于 Promise 的异步编程模型,包括 async 和 await 语法,以及 then 和 catch 方法。这种机制能够有效提升应用的响应性和用户体验。
并行化能力在并行化能力方面,鸿蒙 OS 提供了两套基础实现方式。开发者可以通过 RTS(运行时系统)实现并行化,也可以通过底层库(如 C++ 标准库中的 )实现。不过,如果完全依赖底层库,可能会导致开发效率下降。为了满足业务需求,鸿蒙 OS 在年初引入了 Worker 和 TaskPool 能力。Worker 类似于传统的线程模型,每个 Worker 都有独立的内存空间和执行单元,支持通过消息进行通信。消息可以包含可序列化的数据,也可以通过指针直接迁移数据。TaskPool 则类似于线程池,能够动态管理线程数量,支持标记为 @concurrent 的函数直接在任务池中调度和运行。与安卓平台的线程池不同,鸿蒙 OS 的 TaskPool 会根据硬件条件和任务负载动态调整线程数量。这种机制避免了安卓平台中因线程池数量过多而导致的系统资源消耗问题。
接下来我们对比鸿蒙 OS 的 Worker 并行化能力和安卓端的相关特性。从多个维度来看,Worker 本质上不推荐手动创建,而是通过系统配置 build-provider.json 绑定 ETS 文件来实现创建。这一点与安卓端并无明显差异,安卓端可以通过 THREAD 等方式启动线程。
在鸿蒙 OS 5.0 以下版本(如 4.2 版本)中,主要运行的仍然是安卓系统。这种情况下,安卓线程数量存在上限,这对应用开发者来说是一个挑战。如果 SDK 集成过多,线程数可能超标,进而导致应用被系统强制终止,或出现业务场景异常崩溃等稳定性问题。
数据传输方面,鸿蒙 OS 为了优化 Worker 的性能和负载,对 Worker 的数量和单个 Worker 的传输上限进行了限制。鸿蒙 Worker 的单个传输上限类似于安卓中的 Binder 机制,也存在类似的传输限制。不过,安卓线程通常没有严格限制,因为线程本质上是一个内存拷贝过程,除非开发者通过指针等方式自定义线程间数据传输。
在传输格式上,鸿蒙 OS 支持通过 Sendable 接口进行数据传输。Sendable 是一种注解方式定义的数据结构,具有传染性,即如果一个类被标记为 Sendable,其关联属性也必须是 Sendable 类型。鸿蒙 OS 支持基础数据类型(如 number、string)和集合类型作为 Sendable 传输的内容。对于跨模块调用,鸿蒙 OS 不允许 Worker 跨 HAP 或跨 HSP 调用。相比之下,安卓应用通常运行在一个或多个 Dex 文件中,允许跨 Dex 或跨模块的线程间调用。
TaskPool 类似于双端的协程概念,是一种轻量级线程,仅存储函数。不过,TaskPool 与协程有所不同,它独立于任务维度,且任务执行时长有限制(超过 3 分钟会被系统自动回收)。安卓平台可以通过 ASM 插桩技术对线程的创建和执行进行监控和优化,但轻量级线程或协程的实现通常依赖于线程池或协程机制。
TaskPool 中的任务默认支持数据转移(transfer),不支持拷贝。此外,TaskGroup 不支持 SDK 初始化包的加载。某些同学习惯在异步线程中触发 SDK 的行为,在鸿蒙 OS 上可能会因 TaskPool 生命周期结束而导致变量被释放。
关于并行化数据传输的 Sendable 概念,Sendable 通过系统提供的 SharedHeap(共享堆)实现传输。共享堆与本地堆(local Heap)的区别在于,共享堆支持 Sendable 化数据的传输,而本地堆则需要序列化。共享堆的管理和控制耗费了华为专家大量时间和精力,其中还涉及复杂的异步锁(async lock)机制。在 RTS 并发实例期间(包括 Worker、TaskPool 等),数据可以通过 Sendable 传递,但 Worker 需要使用单独的 API。TaskPool 则完全支持 Sendable 的直接传输。这种异步锁机制允许在 TaskPool 或 Worker 中锁定其他任务中的某些函数,实现线程间的同步,类似于安卓中的 synchronized 或其他锁机制。
小红书在一些典型化场景中已经实现了并行化处理。例如,网络请求是一个典型的耗时操作,因为请求过程中涉及验签和安全能力的处理,这些操作如果在主线程中同步完成,可能会导致应用掉帧。当用户滑动时,掉帧现象会非常明显,这通常是由于大量计算引起的。为了解决这一问题,我们采用了 Worker 化的方式,将这些操作移到 Worker 线程中,从而避免主线程的卡顿。
在进行埋点时,可能会涉及数据库的 IO 操作,这些操作也不建议在主线程中执行。通过将这些操作放到 Worker 线程中,可以有效避免对主线程的影响。
针对双列布局中的图片和资源预加载,我们采用华为自研的 RCP 网络解决方案(类似于 HTTP),通过 Worker 线程在远端进行下载,并在完成后将结果返回到主线程。此外,TaskPool 的应用场景也非常广泛,例如文件上传、多媒体操作以及启动任务的编排等。TaskPool 的优势在于轻量化,避免了线程上下文切换带来的不必要耗时。
关于冷启动和首刷场景的优化。这部分主要包括两个方面:模块的懒加载和动态组件的复用池。懒加载是应用开发中常见的优化手段,类似于安卓端的 class order 机制。当应用不需要某个类时,可以延迟加载该类,直到真正需要使用时才加载。这种方式可以显著提高冷启动阶段的代码加载效率,从而大幅降低冷启动时长。
动态组件和组件复用池则是为了解决 UI 组件重复创建的问题。在应用中,可能会有多种相同类型的 UI 组件(例如小红书中的笔记组件)。为了避免重复创建带来的开销,我们希望在运行时尽量复用已有的组件,而不是频繁地创建和销毁。
类前端视角下的模块懒加载我们通过特定的分析工具对懒加载进行了深入分析。如图所示,我们能够识别出启动过程中加载的各种模块,包括 RNOH(React Native on Harmony)、Web engine(网页引擎)、Red Player(播放器)等组件。这些模块的加载过程涉及到多个.so 文件,即共享对象文件。
通过自上而下的分析方法,我们可以清晰地看到每个模块加载的具体耗时。进一步分析这些.so 文件与 RTS(运行时系统)的关联,以及它们所引入的 Napi 的 TS 文件。我们进行了懒加载潜在对象的分析,发现许多 RTS 实际上并不需要的类文件已经被加载。这是因为开发者在编写代码时,可能并未充分考虑到导入一个类或方法对应用启动延迟的影响。
为了优化这一过程,我们的目标是减少字节码中需要加载的类文件数量,从而加快应用的冷启动速度。华为提供的编译器能够将 RTS 编译成 Ark bytecode(方舟字节码),这是一种高效的字节码格式。通过减少需要加载的类文件数量,我们可以显著提高应用的启动速度。
华为还提供了一种懒加载的导入方式,只有在真正需要使用某个类时,它才会被加载。这种懒加载机制有助于减少应用启动时的资源消耗。这引发了一个问题:为什么华为不默认采用全懒加载方式,即只有在使用时才加载类文件呢?我已经将这个问题反馈给华为,并且系统侧可能会考虑在未来的版本中默认采用懒加载方式,同时仍然允许用户手动选择非懒加载的方式进行类文件的加载。
在小红书的首页场景中,笔记卡组件在多个场景中被复用。为了避免重复创建 UI 导致的性能消耗,我们采用了动态组件的概念。动态组件的核心原理是利用占位符来延迟组件的创建,这与 Android 开发中使用 Stub 模式的概念相似。在这种模式下,可以使用一个代理对象(stub)来代表尚未初始化的组件,从而延迟组件的创建过程。当真正需要渲染组件时,再将渲染内容填充进去,从而避免每次调用构建函数(如 build)时的耗时。
占位逻辑通过系统的 API 实现,涉及到 NodeContainer 和 NodeController 的绑定关系。Container 和 Controller 一一映射,由 NodeCore 进行管理。Container 仅管理当前展现的内存部分,使用完毕后需要将其放回池中进行回收和再利用。以冷启动首刷为例,在启动阶段可以先获取磁盘上的笔记内容,然后在 BuilderNode 中预先创建多个 Image 组件。这样,在等待网络或推荐接口响应时,Image 组件已经创建完毕,从而在首页刷新时可以立即使用这些组件,这对于提高首刷非常有益。
对于组件复用池,当动态组件不再使用时,需要将其返回到组件池中。对于自定义组件,通过 NoteContainer 占位方式,由 NodeController 进行管理。在需要创建子组件时,先在 NodePool 中查找,如果找不到,则创建新组件;如果找到,则尝试复用。流程图展示了从 Container 装载 NodeItem 开始,通过 NodePool 查找,如果找到则进行条件判断和复用。
组件的新建和复用过程中,如果找到对应的 NodeItem,则调用 build 方法并更新自定义组件的状态,完成复用。如果有对应的 NodeItem,可以直接通过 update 函数更新内部状态并刷新 UI。但要注意,update 方法可能会因状态变量过于复杂而导致更新延迟,出现图像残影。因此,需要拆分 state,使其足够小,以确保状态变更到通知 UI 的时间缩短,消除残影。
我们的策略是优先在 NodePool(节点池)中查找可用的 NodeItem(节点项)。如果 NodePool 中存在可用的 NodeItem,我们就直接使用它,并通过 getNode 方法进行 item 绑定,随后更新其状态以实现复用。如果 NodePool 中没有找到对应的 NodeItem,那么我们将通过 makeNode 方法调用 build 函数来创建新的节点项。
完成组件的复用后,我们需要将这些组件返回到缓存池中,以便在未来可以再次使用。这个过程涉及到 NodeContainer(节点容器)和 NodeController(节点控制器)的销毁,并将 NodeItem 重新放回 NodePool 中。为了更有效地管理缓存,业务层可以利用 LRU(最近最少使用)算法,或者鸿蒙系统提供的 LRUCache 和 LiUHashMap 等数据结构,来自定义缓存的大小,从而优化组件的复用和缓存策略。
滑动类场景在小红书应用中,滑动类场景非常普遍,包括推荐页的子频道、个人页中的收藏点赞以及用户自己发布的笔记,还有搜索结果页中的搜索结果和用户商品等,这些都是双列滑动场景。这些双列滑动场景占据了小红书用户体验的 90% 到 95%,因此,滑动体验的流畅性对于用户的整体体验至关重要。
为了提升滑动场景的流畅性,小红书采用了 RCP 框架来优化网络资源的获取。RCP 是华为提供的一个系统组件能力,主要解决网络资源获取效率问题。通过 RCP,开发者可以在需要时发起网络请求,并自定义资源的写入地址,如文件或 ArrayBuffer。RCP 负责高效地将资源写入指定位置,而在不需要时,可以取消 RCP 请求,从而优化资源管理。
RCP 的核心能力在于能够取消请求,并对弱网场景进行了优化,其建联过程优于 HTTP 1.1 或 2.0。基于 RCP,小红书还应用了华为俄研所提供的 Prefetch 方案。Prefetch 方案在瀑布流组件的可见区变更时,通过 worker 线程(如 prefetched worker)启动资源获取,当不可见时关闭,从而优化快速滑动场景,减少不必要的带宽消耗。
在快速滑动过程中,有些 item 可能短暂消失,对于双端场景,网络请求可能已经发出且在途,无法取消,导致带宽浪费。Prefetch 和 RCP 结合的方式可以优化这种快滑场景,防止真正想要看的内容出现白块。Prefetched worker 线程管理多个 RCP 请求,每个请求都有完整的生命周期。当通过 RCP 请求获取到所需资源时,会通知主线程,主线程根据地址加载资源到 Image 组件或占位符 RQI 组件中。
性能热点问题定位:静态代码检测在小红书的开发过程中,我们遇到了一些性能热点问题,这些问题大多是通过 Code Linter(代码检查工具)检测出来的。由于开发节奏快,开发者在编写代码时可能难以关注到性能问题,因此需要 CI(持续集成)检查工具来辅助检查。常见的性能热点包括:
在列表场景中频繁使用的 LadyForEach 组件,需要指定 key 以实现列表复用。如果开发者忘记指定 key,Code Linter 会报错提示。
在 onClick 或 onVisible 等函数中编写空 callback(回调函数)。当这些空 callback 积累到一定数量(如几百个或上千个)时,可能会严重拖慢应用性能。Code Linter 可以扫描出这类问题。
未使用 TaskPool 处理网络资源。例如,Image Bitmap 直接传递 URL 进行同步加载,当网络阻塞时会导致 UI 线程卡顿。
复杂的 ETS 组件在列表场景下未实现重用。未设置重用的 ETS 组件在列表滚动时需要重新构建,非常耗时。组件嵌套层级过深也会导致性能问题。在安卓端,布局检查器建议容器嵌套不超过四层。
使用 JSON.stringify 进行对象序列化。JSON.stringify 有一定耗时,尤其在处理 100KB 左右的数据时,可能需要 10 毫秒左右。Code Linter 会提示这部分性能问题,但是否需要转异步线程需要开发者自行判断。
调用 Image 的 syncLoad(同步加载)。在某些场景下,如转场动画,需要同步加载 image 以保证连贯性。但如果 image 是非磁盘资源(如网络资源),会导致卡帧。Code Linter 可以扫描出这类问题。
关于编译器的优化。ETS 组件应避免嵌套过深。如果嵌套过深,可以将每层函数通过系统的 builder param 或 builder 函数转换。使用 @builder 注解标识的函数会在编译期间与 ETS 代码整合,从而提高编译器优化效果。
Code Linter 支持全量扫描和基于 Git DIFF 的增量扫描,但目前华为的 Code Linter 还不能与 Git Prehook 关联,导致无法在流水线上自动检查。虽然 CI 检查阶段已有 Code Linter,但本地代码提交阶段仍需手动运行脚本,无法实现自动检查。我们正在催促华为解决这一问题。
在处理 UI 重载场景时,我们采用了一种称为分帧方案的方法。 分帧这个术语的含义是,当应用在一帧内无法完成所有绘制工作,或者在多帧内都无法完成时,会导致屏幕卡顿现象。 尽管用户可以看到画面,但却无法进行滑动或操作。 在这种情况下,分帧方案就显得尤为合适。 虽然分帧方案可能看起来不是最优雅的解决办法,但它确实能够有效地解决性能问题,使应用性能达到预期标准。 分帧方案虽然看似是一种应急措施,但它能够帮助应用性能达标。
分帧方案的流程大致如下:假设我们有数据 a、b、c 需要渲染,未采用分帧方案前,数据 a、b、c 会同时到达并触发状态变更,进而驱动整个 UI 进行刷新。这会导致在一帧内需要绘制大量 UI 组件,从而影响应用性能。为了解决这个问题,我们采用分帧方案,将数据 a、b、c 拆分开,分别在不同的帧中进行渲染。例如,数据 a 在第一帧中渲染完成后,通过调用宏观指令让其进入下一阶段,然后在下一帧中更新数据 b,依此类推。
在小红书的图文笔记场景中,分帧方案得到了应用。当用户在首页的双列场景中点击一篇笔记进入笔记详情页时,这个过程涉及到许多组件的加载。我们可以将这些组件拆分成不同的帧,例如帧 a、帧 b 和帧 c。对于用户而言,他们通常希望在第一时间看到整个大屏的画面,因此我们会优先在帧 a 中展示大图。而在帧 b 和帧 c 中,我们再处理顶部导航栏或底部交互区等内容。通过这种分帧策略,我们能够确保用户在第一时间看到最关键的内容,同时避免了因为一次性加载过多组件而导致的性能问题。
传统的主观工具对于鸿蒙 OS 的性能分析仍然适用。例如,抖音和小红书都通过竞品分析来进行主观测评。这种能力主要是通过录屏来展示整个流程的耗时和时长,特别适合评估冷启动完成时延和转场过程的性能。通过录屏,我们可以逐帧查看用户从点击开始到结束的帧数和真实时长,以此来衡量整个过程的持续时间。
鸿蒙性能分析工具:IDE Profiler除了主观工具,我们还可以使用 IDE 提供的性能分析工具,如 Profiler,来分析慢函数。由于 ArkTS 编程语言框架主要通过 RTS 和 NAPI(原生应用接口)进行关联,因此需要能够查看 ArkTS 和 NAPI 的整个堆栈层级。这与安卓有所不同,因为当 Java 通过 Java Native API 与原生代码交互时,堆栈并不那么容易查看。
在小红书的性能分析中,我们展示了一个整体线程分析的例子。在左侧,可以看到小红书的主线程(如 com 点开头的线程)、Daemon 线程、Worker 线程以及 FFRT 线程。FFRT 是一种运行函数流的线程,可以执行 TaskPool 上的函数。在下图右侧,我们可以看到在 RTS 环境下的分析结果,其中顶部显示了 NAPI 调用,底部则是一些 C++ 函数。整个调用栈和它们的执行时长是通过一种自上而下的视图来展示的。利用这种视图,我们可以精确地识别出哪些慢函数是造成界面卡顿的原因。
DevEco Testing 是一个性能测试工具,它的功能非常全面,性能测试只是其中的一部分。除了性能测试,它还支持多种测试场景,包括 debug testing。在 debug testing 场景中,用户可以自定义业务场景,监测 CPU 的耗时和负载、GPU 的耗时和负载、设备发热情况以及功耗等问题。
使用 DevEco Testing 进行性能测试的过程如下:首先定义测试场景,然后捕获主帧数据。一旦开始捕获,就可以观测到 FPS(帧率)、GPU 负载以及整体功耗等数据。完成性能数据捕获后,工具会生成一份报告,为用户提供了一个完整的场景分析。不过,目前场景定义还缺乏脚本化能力,需要人工操作辅助。未来,我们期望能够实现场景定义的脚本化配置,类似于自动化测试。这样,就可以通过自动化工具,实现更高效的测试流程。
总结 展望在对性能场景进行优化后,我们可以看到显著的收益。在实验室环境下的测试显示,冷启动时间可以降低 50%,响应时延可以低于 100 毫秒,完成时延则保持与双端持平或更优。在流畅性方面,在多场景和重载场景下均实现了 0 丢帧的成果。需要注意的是,这里的测试是在非重载模式下进行的,即没有同时运行多个资源密集型应用,如《王者荣耀》或《和平精英》等。在这种条件下,我们的核心场景,如冷启动、搜索和个人页等,都能够与双端完全对齐。
展望未来,有几个方向。首先,我们希望能够在全场景下实现组件复用,以最大程度地实现 UI 复用。这样可以在多个业务之间的转场或 UI 创建过程中,将不必要的 UI 创建和消耗降到最低。其次,我们正在考虑代码延迟加载的 lazy 机制。华为内部可能将其作为通用的解决方案,但在实施过程中我们发现了许多问题,例如全 lazy 加载可能会影响第三方 SDK,如支付宝等,因为它们可能进行了额外的二进制优化,导致加载失败或无法响应。因此,我们期望通过代码延迟加载来实现持续治理,但目前它可能还不适合全场景的 lazy import。最后,我们关注防劣化问题,即在每个版本发布时,我们不希望性能指标出现劣化。我们希望能够在开发阶段就定义劣化指标和具体数据,以防止应用劣化。这部分可能需要借助 DevEco Testing 和主观测评的方式来实现。包括我们关注的指标,例如冷启动和流畅性等,未来可能会纳入防劣化场景。目前,我们的 CI 环节或 RC 环节,包括流水线的性能管控和代码 CR 机制,都能够规避这类问题。
来源:商财洞察君