本文面向 RNOH 应用开发者与运维人员,针对应用在鸿蒙系统上运行时出现的负载过高与功耗异常类问题,提供从现象识别、排查思路、根因定位到解决方案的完整指导。

负载与功耗类异常区别于崩溃、卡死等稳定性问题:应用通常不会直接退出,而是表现为持续高 CPU 占用、设备发烫、电池快速消耗、界面卡顿掉帧、后台待机耗电等。这类问题往往是"慢性病",在长时间运行或特定场景下才会暴露,定位难度较大。

1.1 涉及的关键线程

RNOH 的线程模型由 TaskExecutor 统一管理,共定义 4 个逻辑线程(TaskExecutor.h:19-28):

enum TaskThread {
  MAIN = 0, // main thread running the eTS event loop
  JS, // React Native's JS runtime thread
  BACKGROUND, // @deprecated
  WORKER, // used by some turbo modules
};

在线程创建阶段(TaskExecutor.cpp:19-32),框架会显式命名两个核心线程:

auto mainTaskRunner = std::make_shared<NapiTaskRunner>("RNOH_MAIN", mainEnv);
auto jsTaskRunner   = std::make_shared<ThreadTaskRunner>("RNOH_JS");

并在 JS 线程启动后将其 QoS 提升到 QOS_USER_INTERACTIVE(TaskExecutor.cpp:29-31):

this->runTask(TaskThread::JS, [this]() {
  this->setTaskThreadPriority(QoS_Level::QOS_USER_INTERACTIVE);
});

负载与功耗排查需要重点关注以下线程:

线程名OS 线程QoS主要职责负载/功耗风险点
RNOH_MAIN复用宿主 napi/uv 循环宿主默认ArkUI 组件生命周期管理、组件树管理、部分 TurboModule、Fabric 挂载mutations执行mutations 执行过长、组件创建/属性设置过多、同步TM调用阻塞
RNOH_JS独立 OS 线程USER_INTERACTIVE(高优先)加载/执行 bundle、React render、ShadowTree 构建/布局/Diff、JS 交互处理JS 长任务、桥调用风暴、React 重渲染、GC 抖动
WORKERworker 线程—部分配置在 worker 的 TurboModuleTM 频繁跨线程通信、负收益场景
FFRT 并行线程系统线程池THREAD_PRIORITY_LEVEL_5ShadowTree/Diff/mutations 并行化、组件预创建并行度过高导致线程饱和

说明:BACKGROUND 线程为实验特性,涉及复杂线程间通信,商用版本不应开启(见架构介绍第 90-94 行)。本文排查聚焦 RNOH_MAIN、RNOH_JS 与后台功耗三类。

1.2 常见现象

负载与功耗异常的典型表现:

  • 前台负载高:界面滚动/动画卡顿、FPS 持续低于目标帧率、设备明显发烫、CPU 占用率长期偏高。
  • 后台功耗异常:应用退到后台后仍频繁唤醒 CPU、待机耗电明显高于预期、日志中可见后台持续输出 trace/log。
  • 启动阶段负载尖峰:冷启动时主线程长时间繁忙,首帧渲染延迟,甚至触发 AppFreeze(THREAD_BLOCK_6S)。
  • 内存/GC 抖动型负载:内存持续上涨触发频繁 GC,GC 期间 JS 线程停顿导致掉帧。
  • 长时运行型:短时表现正常,但长时间运行后性能逐步劣化,常伴随资源未释放(监听器、定时器、VSync 订阅累积)。

1.3 与稳定性问题的边界

类别典型表现是否本文范围
负载/功耗高 CPU、发烫、掉帧、后台耗电是
卡死(AppFreeze)主线程 6s 无响应部分重叠,参见稳定性问题定位指南第 3 章
内存泄漏内存持续增长伴随症状,参见内存优化指导
崩溃SIGSEGV/SIGABRT否,参见稳定性指南第 4 章

负载过高是卡死的常见前置诱因:主线程长时间繁忙会演变为 THREAD_BLOCK_6S。因此本文定位思路与稳定性指南第 3 章"卡死"部分互补,本文侧重"还没到冻屏阈值,但负载/功耗已经异常"的预防和定位。

2. 排查思路与整体分析

负载与功耗问题的排查核心是回答三个问题:哪个线程在负载?负载来自哪段逻辑?是否在后台仍持续? 建议按如下整体流程推进。

2.1 整体排查流程

现象复现
   │
   ▼
① 建立基线(区分 dev/release)
   │
   ▼
② 用性能浮层定位"哪个线程在负载"
   │     ├── UI FPS 低 → RNOH_MAIN 侧
   │     └── JS FPS 低 → RNOH_JS 侧
   │
   ▼
③ 抓取 HiTrace,定位"负载来自哪段逻辑"
   │     ├── 看 TraceSection 区间耗时
   │     └── 看 RNOHMarker 的 FABRIC_* 区间
   │
   ▼
④ 判断"是否在后台仍持续"
   │     ├── 后台 trace 是否有 VSync/Timer 唤醒
   │     └── 检查 ArkTS setInterval 是否绕过 HarmonyTimerRegistry
   │
   ▼
⑤ 结合内存/GC/日志开销综合判断
   │
   ▼
根因定位(见第 3 章)→ 解决方案(见第 4 章)

2.2 步骤一:建立基线,排除环境因素

负载排查的第一步是排除环境干扰,否则容易误判。

区分 debug 与 release 构建。 这是最高频的误判来源:debug 构建下 C++ 侧 DLOG/LOG 默认开启、JS 侧为 Metro dev bundle(未压缩、含大量 dev only 代码、HMR/Inspector WebSocket 常驻),性能远低于 release。具体差异见定位定界指导类FAQ第 101-111 行。所有功耗结论必须以 release 构建为准。

关闭调试通道。 debug 下 JSPackagerClient 通过 ReconnectingWebSocket(ReconnectingWebSocket.ts,重连间隔 RECONNECT_DELAY_MS = 2000)与 Metro 保持长连接,断线自动重连,会持续占用网络与 CPU。release 不应出现这些。

关闭开发态性能浮层。 RNPerfMonitorView 以 setInterval 每 500ms 轮询指标(RNPerfMonitorView.ets),仅 debug 可用,但其轮询本身就有开销,测量完成后应关闭,不要带着浮层做功耗结论。

确认 BiSheng/PGO 等构建优化已正确应用。 见性能调优第 583-720 行。若基线构建优化未启用,所有负载数据都会偏高。

2.3 步骤二:用性能浮层定位"哪个线程在负载"

RNOH 提供一个开箱即用的实时性能浮层 RNPerfMonitorView.ets,可通过 DevMenu 的"Show Perf Monitor"开关调出(DevMenu.ts → devToolsController.togglePerfMonitor())。浮层每 500ms 刷新,展示:

指标来源说明
JS FPSJSFpsMonitor(C++)JS 线程在 VSync 帧窗口内的"产出命中率",反映 JS 线程是否跟上帧节奏
UI FPSUIFpsMonitor(ArkTS displaySync)UI 线程实际刷新帧率
JS VM (MB)getJSRuntimeHeapUsageHermes/JSVM 堆已分配字节
PSS (MB)PerfMonitorController.ets:85-92,hidebug.getPss()应用进程实际物理内存占用
ViewsgetComponentInstancesCount当前 ComponentInstance 数量,可用于判断组件树是否异常膨胀

浮层指标的关键判读逻辑:

  • UI FPS 低 + JS FPS 正常 → 负载在 RNOH_MAIN 侧(Fabric mutations、组件创建、同步 TM 调用)。
  • JS FPS 低 + UI FPS 正常 → 负载在 RNOH_JS 侧(JS 长任务、React 重渲染、桥调用风暴)。
  • JS FPS 高 + UI FPS 低,但界面无明显变化 → 可能存在"空转":JS 频繁被唤醒执行轻量任务但无有效产出,CPU 被白白占用(典型如高频短定时器、未取消的 VSync 订阅)。这是功耗类问题的关键信号。
  • PSS 持续上涨 → 内存侧异常,后续会引发 GC 抖动型负载,转第 3.4 节。
  • Views 持续上涨不回落 → 组件实例泄漏,加重主线程每次 mutations 成本,转稳定性指南第 2 章。

JSFpsMonitor 的实现原理(JSFpsMonitor.cpp:41-54)值得了解,它订阅 UITicker(VSync),在每个 VSync 帧把一个任务投递到 TaskThread::JS,结合 FABRIC_COMMIT_*/FABRIC_UPDATE_UI_MAIN_THREAD_* 等 marker 判定"本帧 JS 是否有有效产出":

tickerUnsubscribe = m_uiTicker->subscribe(
    [this](std::chrono::steady_clock::time_point timestamp) {
      this->m_taskExecutor->runTask(
          TaskThread::JS, [this, timestamp]() { onVSyncFrame(timestamp); });
    });

因此 JS FPS 反映的是"JS 线程能否在帧窗口内完成 React 提交/布局/Diff 等工作",是一个比单纯 CPU 占用更贴合用户体感的负载指标。

2.4 步骤三:抓取 HiTrace,定位"负载来自哪段逻辑"

性能浮层能告诉你"哪个线程负载",但要定位到具体逻辑,必须抓 trace。RNOH 已将完整的 trace 能力接入鸿蒙 HiTrace 体系:

  • **C++ 侧 TraceSection**:通过补丁(patches/react_native_core/extras/TraceSection.h)将 React Native 原生的 facebook::react::TraceSection 替换为调用 OH_HiTrace_StartTrace/FinishTrace 的 CustomTraceSection。因此引擎内所有 TraceSection s("...") 都会落到 HiTrace。
  • **RNOHMarker → HiTraceRNOHMarkerListener**:将 FABRIC_COMMIT_*、FABRIC_DIFF_*、FABRIC_LAYOUT_*、FABRIC_FINISH_TRANSACTION_*、APP_STARTUP_*、RUN_JS_BUNDLE_*、INIT_JS_RUNTIME_* 等 marker 写入 HiTrace(受 WITH_HITRACE_REACT_MARKER 编译开关控制,见 cpp/CMakeLists.txt:8-10)。
  • **JS 侧 Systrace.***:NativeTracing.cpp 通过 JSI 注入 nativeTraceBeginSection/nativeTraceEndSection 等宿主函数,使 JS 代码的 systrace 调用也能落到 HiTrace(受 WITH_HITRACE_SYSTRACE 开关控制)。

抓 trace 的方式:

  • DevEco Studio → Profiler → Trace:可视化查看,支持按线程筛选,可直接看到 RNOH_MAIN、RNOH_JS 上的 TraceSection 区间及耗时。
  • **命令行 hitrace**:导出原始 trace 后用 smartperf 或文本工具分析。

trace 上的关键观察点:

  1. 找 RNOH_JS 线程上连续不断的 #RNOH::MessageQueueThread::runOnQueue 或 #RNOH::RNInstanceInternal::callJSFunction 区间 → 桥调用风暴。
  2. 找 RNOH_MAIN 线程上超长的 #RNOH::MountingManager::doMount 或 FABRIC_UPDATE_UI_MAIN_THREAD_* 区间 → 挂载负载。
  3. 找 FABRIC_FINISH_TRANSACTION 与 FABRIC_COMMIT 之间的 gap → React 提交/Diff 的 JS 侧耗时。
  4. 应用退到后台后,trace 上是否仍有 RNOH_JS/RNOH_MAIN 的活动 → 后台功耗定位的关键证据(第 3.3 节)。

RNOHLogger.ts 提供的 Tracer(RNOHLogger.ts:30-45,基于 hiTraceMeter)会在 startTracing() 返回的停止回调中当区间超过 100ms 时输出 warn 日志(RNOHLogger.ts:179-190),这是一个低成本的"慢区间"预警,适合在 ArkTS 侧快速发现异常耗时。

2.5 步骤四:判断"是否在后台仍持续"

功耗类问题的灵魂在于后台。RNOH 框架本身对后台已做了系统性暂停,需要先理解框架"暂停了什么",再去排查"哪里没暂停"。

框架的后台暂停机制(链路:RNAbility.onBackground → RNInstancesCoordinator.onBackground → 各 RNInstance.moveToBackground → postMessageToCpp('BACKGROUND') → C++ ArkTSMessageHub 广播 → 各监听者)覆盖以下对象:

被暂停对象实现位置行为
JS 定时器(setTimeout/setInterval)HarmonyTimerRegistry.cpp:77-83onBackground 置 isForeground=false 并 pauseTimers();triggerExpiredTimers 在 !isForeground 时早退(:162-164)
UITicker(VSync 心跳)UITicker.h:152-154onBackground 置 m_isPaused=true;tick() 在 m_isPaused 时直接 return(:157-159),不再 requestNextTick
动画驱动(NativeAnimatedTurboModule)NativeAnimatedTurboModule.cpp LifecycleObserverBACKGROUND → stopDisplaySoloist(),停止 VSync 驱动
后台 GC + 图片缓存清理RNInstanceInternal.cpp:492-508onBackground():若开启 enableBackgroundGC 则触发 handleMemoryPressureJs(TRIM_MEMORY_BACKGROUND=40);并 MemCacheCallback::notifyAll(MEMORY_LEVEL_CRITICAL) 清理图片缓存

因此,凡是在后台仍出现 CPU 唤醒的,基本都不是上述框架托管的对象,而是绕过它们的"旁路":

  1. ArkTS 侧直接用 setInterval/setTimeout 的 TurboModule 或业务代码——这些走的是鸿蒙事件循环,不经 HarmonyTimerRegistry,后台不会暂停。框架自身也有此类用法(如 StatusBarTurboModule.ts:167-173 的 16ms 色彩动画 interval),业务代码更常见。
  2. 未取消的 VSync 订阅——第三方代码若直接用 OH_NativeVSync 或未调用 UITicker.subscribe 返回的 unsubscribe 函数,会持续驱动 VSync。
  3. WebSocket 心跳——WebSocketTurboModule.ts 支持 pingInterval 心跳,后台不关闭连接会持续保活。
  4. 未关闭的 debug 通道——ReconnectingWebSocket/JSPackagerClient 断线 2s 重连,仅 debug。

后台排查应对照 hitrace 在应用 BACKGROUND 后是否仍有上述线程的活动区间,逐一比对定位。

3. 根因定位

本章按"主 UI 线程负载"、"RNOH_js 线程负载"、"后台唤醒与功耗"、"内存/GC 抖动型负载"、"日志与 trace 开销"五个维度展开根因定位。

3.1 主 UI 线程(RNOH_MAIN)负载过高

主线程负载过高是卡顿和冻屏的直接前兆。主线程承担 ArkUI 组件生命周期管理、Fabric mutations 挂载、部分 TurboModule 执行。负载过高典型根因如下。

3.1.1 Fabric mutations 挂载过重

SchedulerDelegate::performTransaction(SchedulerDelegate.cpp:780+)是挂载主路径。mutations 从 JS 线程提交后,最终在主线程执行组件的 CREATE/UPDATE/INSERT/REMOVE/DELETE。当单次 transaction 的 mutations 数量极大时,主线程会被长时间占用。

框架已内置多项负载抑制策略,定位时应先理解这些策略是否被触发、是否够用:

  • 初始加载窗口切片:SurfaceLoadState(SchedulerDelegate.cpp:164-244)跟踪每个 surface 的初始加载窗口(前 3s 或前 3 个 transaction),用于决定是否对 insert 做切片。
  • 非 create mutations 分批:当非 create mutations 数量 ≥ NONCREATE_SPLIT_MIN_COUNT = 60 且成本 ≥ NONCREATE_SPLIT_MIN_COST = 120 时,splitNonCreateMutationsIntoTwoBatches 会拆成两批跨两帧执行(SchedulerDelegate.cpp:144-145, 395+)。
  • 大文本尾部切片:isTextHeavyInsertTail(SchedulerDelegate.cpp:376-393)当尾部 Paragraph 插入 ≥ 200 个时,按 70 一批切片,依据是"平均 60µs/mutation,70 ≈ 4.2ms 帧的一半"。
  • FlashList 串行强制:shouldForceSerialForSurface(SchedulerDelegate.cpp:317-341)识别 AutoLayoutView/CellContainer,对该 surface 强制串行 10 分钟,避免并行竞态与多余线程唤醒。

定位方法:在 trace 上观察 FABRIC_UPDATE_UI_MAIN_THREAD_START/END 区间长度,以及 FABRIC_FINISH_TRANSACTION 的频率。若单次挂载区间接近或超过一帧(120Hz 下约 8.3ms,60Hz 下约 16.6ms),即可判定为 mutations 过重。ComponentInstanceProvider 的预分配还会在 onUITick 中按帧节流,当距离 120Hz 帧截止不足半个帧时主动暂停预分配(ComponentInstanceProvider.cpp:171,shouldPausePreallocationToAvoidBlockingMainThread),这是主线程保护机制,若被频繁触发说明预分配请求积压严重。

3.1.2 组件创建/属性设置成本

CAPI 架构下组件创建与属性设置在 C 侧直接进行,避免了跨语言数据格式转换(见架构介绍第 29-34 行),但单个组件的创建仍有成本。当业务侧一次性渲染大量节点(长列表未虚拟化、深层嵌套一次性展开),主线程会集中承压。

定位时关注 trace 上 #RNOH::MountingManager::doMount 区间内的密集 CREATE,以及 TextStorageProvider 的 cache hit/miss(TextStorageProvider.cpp:154-157 有对应 TraceSection)——文本 Paragraph 的构建若频繁 cache miss,会成为主线程热点。

3.1.3 同步 TurboModule 调用阻塞主线程

ArkTSTurboModule 的 callSync(ArkTSTurboModule.cpp:103-110+)在调用线程同步执行 ArkTS 方法。若该 TurboModule 声明在 MAIN 线程运行(m_ctx.turboModuleThread,ArkTSTurboModule.h:216),且方法本身耗时(大 JSON 解析、文件 I/O、重计算),会直接阻塞主线程。

每个 TM 调用都被 facebook::react::TraceSection 包裹(ArkTSTurboModule.cpp:92, 106, 154, 214, 255),trace 上可直接看到 ModuleName::methodName 区间。定位时找主线程上的长 TM 区间即可。

解决方案见 4.3:将耗时 TM 迁移到 worker 线程。

3.1.4 FFRT 并行化导致的线程饱和

当 PARALLELIZATION_ON 定义、API ≥ 22 且 ParallelCheck::IsParallelizationEnabled() 为真时(ParallelCheck.cpp),mutations 的 create 部分会用 FFRT 并行(ffrt::job_partner<ScenarioID::MUTATION_PARALLELIZATION>,SchedulerDelegate.cpp:812-849),最大并行度 MAX_THREAD_NUM_MUTATION_CREATE = 4,QoS THREAD_PRIORITY_LEVEL_5(FFRTConfig.h)。ShadowTree 并行与预创建并行各限 2 线程。

正常情况下并行化降低单帧延迟,但当设备核数有限或同时存在其他高负载时,4 个并行线程 + 主线程 + JS 线程会饱和 CPU,反而表现为整机 CPU 占满、功耗上升。此外弹窗/折叠态下 shouldDisableSchedulerParallelForModalFold(SchedulerDelegate.cpp:195)会临时关闭并行,若该开关频繁翻转,说明场景在模态/折叠间抖动,需排查业务侧的状态切换。

定位:trace 上观察 FFRT 线程组的繁忙度与主线程 doMount 的关系;若整机 CPU 接近 100% 但单帧延迟未改善,考虑临时关闭并行化验证是否为并行饱和。

3.2 RNOH_js 线程负载过高

JS 线程执行 bundle、React render/reconcile、ShadowTree 布局/Diff,并通过 MessageQueueThread 接收所有发往 JS 的任务(MessageQueueThread.cpp:21-31,每个 runOnQueue 都 runTask(TaskThread::JS, ...))。JS 线程负载过高会直接导致掉帧(JS 来不及在一帧内完成提交)。

3.2.1 JS 长任务阻塞

最直接的根因:JS 业务代码执行了同步长任务(大循环、复杂计算、同步 JSON 序列化巨型对象、同步加密)。由于 JS 单线程,长任务会阻塞整个 RNOH_JS 事件循环,期间所有定时器、callJSFunction、EventBeat induce 都被延后。

定位方法:

  • JSFpsMonitor 持续低值:JS FPS 长时间低于目标,且 UI FPS 也随之走低。
  • HiTrace 上 RNOH_JS 线程出现长区间无切换:MessageQueueThread::runOnQueue 区间异常长。
  • AppFreeze 旁证:当主线程因等待 JS 结果被阻塞超 3s/6s,HiCollieFreezeHandler 会捕获 JS 栈(HiCollieFreezeHandler.cpp 的 OH_THREAD_BLOCK_3S/OH_THREAD_BLOCK_6S,FreezeStackCollector.cpp:160-185 用 Hermes sampling profiler 抓栈)。即使未到冻屏阈值,freeze 日志中的 JS 栈也是定位 JS 长任务的利器。

3.2.2 桥调用风暴(JS ↔ Native 高频往返)

RNInstanceInternal 上多个关键入口都有 TraceSection:callJSFunction(RNInstanceInternal.cpp:306)、updateState(:323)、onUITick(:333)、emitComponentEvent(:448)。当业务侧高频触发 native → JS 调用(如高频传感器、高频滚动事件透传、动画每帧通过桥而非 NativeAnimatedTurboModule),JS 线程会被这些小任务填满,表现为"JS FPS 不低但无有效产出"的空转功耗。

定位:trace 上统计 callJSFunction/emitComponentEvent 区间密度。若每帧数十次,即为风暴。

3.2.3 React 重渲染

React render 本身在 JS 线程,但若业务侧因 state 频繁变更、props 大对象每次新引用、列表未做 memo 导致全量重渲染,JS 线程会持续忙于 reconcile + ShadowTree 构建 + Diff,并不断向主线程提交 mutations,形成"JS 忙 + 主线程忙"双高。

定位:trace 上观察 FABRIC_COMMIT_*/FABRIC_DIFF_* 的频率与单次耗时,结合 JSFpsMonitor 的 marker 监听(JSFpsMonitor.cpp:156-173 监听 FABRIC_* start/end 来计算 JS 工作量)。若 commit 频率远超视觉变化需要,即为重渲染。

3.2.4 EventBeat 帧节奏

EventBeat(EventBeat.cpp:34-49)在 JS 线程被请求时订阅 UITicker,下一帧 induce RuntimeScheduler 后立即取消订阅(单次模式)。这是正常机制,但若业务侧高频触发 requestFrame(如 rAF 风暴),会反复订阅/取消,虽然单次开销小,但累积形成 JS 线程的帧节奏负载。排查 rAF 的调用密度即可。

3.3 后台唤醒与功耗异常

后台功耗是负载类问题中最隐蔽的一类。如 2.5 节所述,框架已系统性暂停了托管对象,后台异常几乎都来自"旁路"。本节给出具体定位点。

3.3.1 ArkTS 侧 setInterval/setTimeout 绕过 HarmonyTimerRegistry

这是后台功耗的最高频根因。HarmonyTimerRegistry 只接管 JS 侧 setTimeout/setInterval/requestAnimationFrame(实现 facebook::react::PlatformTimerRegistry),而 ArkTS 侧直接写的 setInterval/setTimeout 走鸿蒙事件循环,后台不暂停。

框架自身也存在此类用法,定位时可对照:

  • StatusBarTurboModule.ts:167-173:setInterval(..., 16) 用于状态栏色彩动画,完成后清理(:171)/出错清理(:179)。前台期间高频,若未正确清理会持续。
  • RNPerfMonitorView.ets:73-93:500ms 轮询,debug only,aboutToDisappear(:67-71)与关闭时(:103-105)清理。
  • WorkerThread.ts:32-39:100ms 轮询直到 ACK,短期。
  • ReconnectingWebSocket.ts:2s 重连,debug only。

业务侧更常见的反例:在 TurboModule 内用 setInterval 做轮询/心跳、在组件 aboutToAppear 起 interval 但 aboutToDisappear 未 clear、用 ArkTS setInterval 驱动本应由 NativeAnimatedTurboModule 完成的动画。这些在后台持续唤醒鸿蒙事件循环线程,是后台耗电的主要来源。

定位:后台 trace 上若见鸿蒙事件循环线程(非 RNOH_JS/RNOH_MAIN)周期性活动,对照上述 setInterval 来源排查。代码层面全局搜索业务 ArkTS 的 setInterval/setTimeout,核对是否在 onBackground/aboutToDisappear 清理。

3.3.2 VSync 订阅未取消

UITicker 在首个订阅者到来时 requestNextTick,并持续请求下一帧直到无订阅者(UITicker.h:63-76,:188-190)。subscribe 返回一个 unsubscribe 函数,必须调用才能停止。若第三方组件/HostObject 直接订阅 UITicker 或自行 OH_NativeVSync_Create 而未在销毁时释放,VSync 会持续驱动,即便界面无变化也在每帧唤醒 CPU,这是典型的"前台空转 + 后台若未暂停则持续耗电"。

框架内部都是规范用法:EventBeat 单次订阅后立即取消(EventBeat.cpp:43-47),JSFpsMonitor 在 stop/析构时取消(JSFpsMonitor.cpp:30-33, 56-65),ComponentInstanceProvider 单次模式。排查重点在第三方与业务自定义 native 代码。

定位:后台 trace 上看是否有名为 UITicker 或自定义 VSync 的周期性回调;代码层面核对所有 UITicker::subscribe/OH_NativeVSync_Create 调用点是否成对释放。NativeVsyncHandle 是 RAII(NativeVsyncHandle.h:41-45 析构 OH_NativeVSync_Destroy),优先用它而非裸 API。

3.3.3 WebSocket 心跳保活

WebSocketTurboModule.ts 支持 pingInterval/pongTimeout 心跳(:18-36,pingInterval=0 禁用,需 API 21+)。连接未在 onBackground 关闭时,心跳会持续保活网络连接,唤醒基带/CPU。ping 方法未实现(:195-201 仅 warn)。

定位:后台 trace/网络日志看 WebSocket 连接是否在后台仍有收发;代码层面在 onBackground 关闭非必要 socket 或将 pingInterval 设为 0。

3.3.4 后台 GC 与图片缓存清理的开销

RNInstanceInternal::onBackground(RNInstanceInternal.cpp:492-508)在开启 enableBackgroundGC 时会主动触发一次 GC(handleMemoryPressureJs(TRIM_MEMORY_BACKGROUND=40)),并清理图片缓存(MEMORY_LEVEL_CRITICAL)。这是一次性的后台清理,本身是省电/省内存的正向行为,但若:

  • 频繁前后台切换(业务侧在 onForeground/onBackground 间抖动,见 RNInstance.ts:1348-1364 的 shouldEnableBackgroundModeOnInvisible && !hasVisibleSurface() 守卫),会反复触发 GC,反而增加负载。
  • 图片缓存过大,单次 Clear() 释放大量对象引发瞬时主线程压力。

定位:trace 上看 #RNOH::RNInstanceInternal::onBackground 与 onMemoryLevel 区间频率;若短时间内多次出现,排查前后台切换抖动来源。

3.4 内存/GC 抖动型负载

内存与负载强相关:堆增长 → 频繁 GC → GC 期间 JS 线程停顿 → 掉帧;同时 GC 本身消耗 CPU。这类"由内存侧引发的负载"容易被忽略。

3.4.1 内存压力链路

鸿蒙 AbilityConstant.MemoryLevel(0/1/2 = MODERATE/LOW/CRITICAL)经 RNAbility.onMemoryLevel(RNAbility.ets:235-240)→ RNInstancesCoordinator(:426-429)→ NapiBridge.onMemoryLevel → RNInstanceInternal::onMemoryLevel(RNInstanceInternal.cpp:471-490),映射为 Android 的 5/10/15 调 handleMemoryPressureJs,并 ImageCachePro::MemCacheCallback::notifyAll(ImageCacheMemoryCache.cpp:67-94,CRITICAL 时 Clear(),MODERATE/LOW 时 ClearHalf())。

若内存频繁触及压力阈值,会反复触发上述链路,表现为周期性 CPU 尖峰。定位:性能浮层的 PSS 曲线 + onMemoryLevel 日志频率。

3.4.2 JS 堆增长与 GC 频率

getJSRuntimeHeapUsage(RNInstanceInternal.cpp:801-840)可读取 Hermes hermes_allocatedBytes 或 JSVM usedHeapSize。性能浮层的 "JS VM (MB)" 即此值。若该值在长时运行后持续爬升且回落迟缓,说明 GC 跟不上分配速度,会转化为 JS 线程停顿型掉帧。

Hermes 有内存水位线机制 HermesMemoryWaterMark(默认 1434MB,RNInstance.ts:550 可配 hermesMemoryWaterMark),超水位后框架会记录一次性告警(tryMarkHermesWaterMarkExceededForProcessLifetime),定位时可作旁证。

内存侧的完整优化见内存优化指导,本文不重复。

3.5 日志与 trace 开销

一个常被忽视的负载来源是日志本身。

3.5.1 C++ 日志(DLOG/LOG)

RNOH C++ 侧大量使用 DLOG(INFO)/LOG(INFO)/RNOH_ASSERT(如 TaskExecutor.cpp、RNInstanceInternal.cpp、SchedulerDelegate.cpp、ArkTSTurboModule.cpp 等)。日志详尽度由 CMake 的 LOG_VERBOSITY_LEVEL 控制(cpp/RNOH/CMakeLists.txt:53-57)。debug 构建默认详尽,release 调优时应设为 0 关闭打印(见性能调优第 577-581 行):

set(LOG_VERBOSITY_LEVEL 0)

若 release 构建仍保留高详尽度日志,高频路径(每帧、每次 TM 调用)的日志 I/O 会成为可观的 CPU 与功耗开销。

3.5.2 ArkTS 日志(hilog)与 Tracer

RNOHLogger(RNOHLogger.ts)基于 hilog,按 minSeverity 过滤(:141-155)。Tracer 基于 hiTraceMeter,开销低,但 startTracing() 在 >100ms 时会 warn(:179-190)。若业务侧在热路径大量调用 logger,hilog 的序列化与内核写入会累积开销。

定位:trace 上观察 hilog 相关区间;代码层面核查热路径是否遗留 debug 级日志。

3.5.3 trace 区间过密

TraceSection 是 RAII,进入/退出各一次 HiTrace 调用。正常使用开销极低,但若在每帧的紧密循环中高频进入退出(如内层循环里创建 TraceSection),trace 本身的写入会成为负载,且 trace 文件膨胀拖慢分析。原则:trace 区间粒度对齐"逻辑步骤",不要进到内层循环。

3.6 启动阶段负载尖峰

冷启动时 bundle 执行、首帧布局、首批组件创建集中发生,是负载尖峰高发区。RNOHMarker 提供完整启动 marker:APP_STARTUP_*、RUN_JS_BUNDLE_*、INIT_JS_RUNTIME_*、REACT_INSTANCE_INIT_*、TURBO_MODULE_SETUP_*、CONTENT_APPEARED,经 PerformanceMetricsRegistry 聚合(getSnapshot() 返回 folly::dynamic),性能浮层可展开查看(PerfMonitorController.ets:116-124 的 getPerformanceMetrics)。

启动负载定位:抓启动 trace,按上述 marker 序列找到最长的一段,对症处理(bundle 瘦身、延迟非首屏模块、并行预创建是否生效)。启动优化的完整实践见性能调优与性能优化实践,本文不重复。

4. 解决方案

本章按根因维度给出解决方案,与第 3 章一一对应。

4.1 主 UI 线程负载的解决

4.1.1 降低 mutations 单次成本

  • 列表虚拟化:确保使用 FlatList/SectionList(对应鸿蒙侧识别为 AutoLayoutView/CellContainer,框架会强制串行 10 分钟,见 3.1.1),避免一次性渲染全量节点。
  • 分批提交:业务侧将超大批量数据更新拆为多次小提交,让框架的切片阈值(NONCREATE_SPLIT_MIN_COUNT=60、文本 200)更易触发并均匀分散到多帧。
  • 属性 Diff 生效:CAPI 架构支持属性 Diff 避免重复设置(见架构介绍第 33 行),确保业务侧未在每帧强制全量刷新属性。

4.1.2 利用预分配机制

ComponentInstanceProvider 的预分配(ComponentInstancePreallocationRequestQueue)会在主线程空闲帧预创建组件实例,且有"距帧截止不足半帧则暂停"的保护(ComponentInstanceProvider.cpp:171)。业务侧可在首屏渲染前通过预触发已知节点类型的创建请求填充预分配队列,减轻首屏主线程峰值。

4.1.3 关闭/调整 FFRT 并行化

当确认负载来自并行线程饱和(3.1.4),可:

  • 临时关闭验证:将 ParallelCheck 的 PARALLEL_RUNTIME_SWITCH 置 false(仅排查用)。
  • 调整 FFRTConfig.h 的线程上限(MAX_THREAD_NUM_MUTATION_CREATE=4、shadow tree=2、precreated=2)适配目标设备核数。

并行度调整属于框架级调优,需充分回归测试,非业务侧常规手段。

4.2 RNOH_js 线程负载的解决

4.2.1 消除 JS 长任务

  • 将同步重计算迁移到 worker 线程(借助 WORKER 线程的 TurboModule,见 4.3)。
  • 大对象 JSON 解析拆分或流式化。
  • 避免在 JS 主循环里做同步加密/哈希巨型数据。

4.2.2 削减桥调用风暴

  • 滚动/手势等高频事件优先用 NativeAnimatedTurboModule(数据计算在 C++,不跨桥),而非每帧 callJSFunction。
  • 传感器数据节流后再上报 JS。
  • 高频 native→JS 通知合并批处理。

4.2.3 抑制 React 重渲染

  • 列表项 memo + 稳定 key。
  • props 避免每次新引用(用 useMemo/useCallback)。
  • state 拆分粒度,避免大对象 setState 触发全树 reconcile。

4.3 TurboModule 迁移到 worker 线程

将耗时 TurboModule 从 MAIN 迁到 WORKER 线程,是降低主线程负载的最直接手段。原理、使用场景、避免场景与实现步骤见性能调优第 751-794 行。要点回顾:

  • 在 RNAbility 子类重写 getRNOHWorkerScriptUrl 返回 worker 脚本路径。
  • 在 ArkTSTurboModule 的 callSync/callAsync 按配置切换到 worker 线程。
  • 避免负收益场景:如 ImageLoaderTurboModule 由 UI 线程发起,迁到 worker 会导致频繁线程切换反而更慢。

4.4 后台功耗的解决

4.4.1 清理 ArkTS 侧旁路定时器

  • 全局搜索业务 ArkTS 的 setInterval/setTimeout,逐个确认:
    • 是否必须在后台运行?否 → 在 onBackground/aboutToDisappear clear。
    • 是否本应由 NativeAnimatedTurboModule 承担(动画类)?是 → 改用 Animated。
  • 框架自身的 StatusBarTurboModule 色彩动画等,确认完成后及时清理 interval。

4.4.2 取消 VSync 订阅

  • 所有 UITicker::subscribe 的返回值(unsubscribe 函数)必须在销毁路径调用。
  • 优先用 RAII 的 NativeVsyncHandle 与单次模式的 VSyncListener/EventBeat,避免裸 OH_NativeVSync_Create。
  • 第三方 native 组件审计其 VSync 生命周期。

4.4.3 后台关闭非必要网络

  • onBackground 关闭 WebSocket 或将 pingInterval 设 0。
  • 长连接改为前台活跃、后台静默策略。
  • HttpClient 是每请求新建(HttpClient.ts:111-149),无连接池保活,本身不构成后台耗电,但业务侧自定义长连接需审计。

4.4.4 稳定前后台切换

  • 避免在 onForeground/onBackground 间抖动(会反复触发 onBackground 的 GC 与图片缓存清理)。
  • 理解 RNInstance 的 shouldEnableBackgroundModeOnInvisible && !hasVisibleSurface() 守卫(RNInstance.ts:1351-1356),避免无可见 surface 时仍切前台。
  • 谨慎使用 enableBackgroundModeOnInvisible,按场景评估。

4.5 内存/GC 抖动的解决

  • 注册 JS 侧 AppState.addEventListener('memoryLevelChange', ...)(AppStateTurboModule.ts:59-76)在收到 memoryLevelChange/memoryWarning 时主动释放业务缓存。
  • 开启 enableBackgroundGC(RNInstance.ts:598-616)让后台主动 GC,但前提是前后台切换稳定(4.4.4)。
  • 图片侧用 resize/合理缓存上限,配合 ImageCacheMemoryCache 的 Clear()/ClearHalf() 自动响应内存压力。
  • 完整内存优化见内存优化指导。

4.6 日志与 trace 开销的解决

  • release 构建设 LOG_VERBOSITY_LEVEL=0 关闭 C++ 日志打印。
  • 确认 WITH_HITRACE_SYSTRACE/WITH_HITRACE_REACT_MARKER 在 release 按需开关(默认 release 可关,避免 trace 写入开销;定位时再开)。
  • 业务侧热路径避免遗留 debug 级 hilog;RNOHLogger 的 minSeverity 在 release 调高。
  • trace 区间不进内层循环。

4.7 负载/功耗排查速查表

现象首查项代码/工具位置
启动慢APP_STARTUP_*/RUN_JS_BUNDLE_*/INIT_JS_RUNTIME_*/CONTENT_APPEARED markerPerformanceMetricsRegistry::getSnapshot、性能浮层
JS 抖/掉帧JSFpsMonitor + FABRIC_* markerJSFpsMonitor.cpp、HiTrace
UI 抖/掉帧FABRIC_UPDATE_UI_MAIN_THREAD_*、doMount 区间SchedulerDelegate.cpp、HiTrace
冻屏前兆HiCollieFreezeHandler 3s/6s 抓的 JS 栈HiCollieFreezeHandler.cpp、faultlog
后台耗电后台 trace 是否有 VSync/Timer/hilog 活动HarmonyTimerRegistry、UITicker、ArkTS setInterval 排查
内存/GC 抖动PSS + JS VM + onMemoryLevel 频率性能浮层、RNInstanceInternal.cpp:471-508
整机 CPU 满FFRT 并行线程组 + 是否并行饱和FFRTConfig.h、ParallelCheck.cpp

5. 经验总结

5.1 核心原则

  1. 先建基线,再下结论:任何负载/功耗结论必须以 release 构建为准,关闭 debug 通道与性能浮层。
  2. 先分线程,再分逻辑:用性能浮层的 JS FPS / UI FPS 先二分到 RNOH_JS 或 RNOH_MAIN,再用 HiTrace 定位具体区间,避免盲猜。
  3. 后台问题先查旁路:框架托管对象(定时器、UITicker、动画)后台已暂停,后台异常优先排查 ArkTS 旁路 setInterval、未取消的 VSync 订阅、WebSocket 心跳。
  4. 负载与内存联动看:GC 抖动型负载常被当纯性能问题误处理,PSS + JS VM 曲线是必要旁证。
  5. trace 粒度对齐逻辑步骤:不要把 trace 区间塞进内层循环,否则 trace 本身成为负载且难以分析。

5.2 常见误区

  • 误区一:用 debug 构建测功耗。debug 的 dev bundle、HMR/Inspector 长连接、DLOG 全开,功耗数据无意义。
  • 误区二:只看 CPU 占用率。高 CPU 不等于用户体感差——JS FPS 高 + UI FPS 低但界面无变化的"空转"才是真功耗,CPU 占用率可能并不高但持续唤醒。
  • 误区三:以为后台一切都会暂停。只有经 HarmonyTimerRegistry/UITicker/NativeAnimatedTurboModule 托管的对象会暂停,ArkTS 旁路定时器与未释放的 VSync 订阅不会。
  • 误区四:盲目开 FFRT 并行化降负载。核数有限时并行饱和反而升高整机功耗,需对照 trace 验证。
  • 误区五:把所有 TM 都迁 worker。由 UI 线程发起的 TM(如 ImageLoaderTurboModule)迁 worker 会导致频繁线程切换,负收益。

5.3 排查清单

发布前/复现负载异常时,按序核对:

  • release 构建且 LOG_VERBOSITY_LEVEL=0,debug 通道与性能浮层关闭
  • BiSheng/PGO 等构建优化已应用
  • 性能浮层 5 项指标(JS FPS/UI FPS/JS VM/PSS/Views)采集到位
  • HiTrace 已开启 WITH_HITRACE_SYSTRACE/WITH_HITRACE_REACT_MARKER 并抓取
  • 前台负载:区分 JS 侧(重渲染/桥风暴/长任务)与主线程侧(mutations/组件创建/同步TM)
  • 后台功耗:trace 在 BACKGROUND 后无 RNOH_JS/RNOH_MAIN 活动
  • 业务 ArkTS setInterval/setTimeout 全部在 onBackground/aboutToDisappear 清理
  • 所有 UITicker::subscribe/OH_NativeVSync 成对释放
  • WebSocket 在后台关闭或 pingInterval=0
  • 前后台切换不抖动(不反复触发 onBackground GC)
  • 内存曲线稳定,无频繁 onMemoryLevel 触发
  • 列表已虚拟化、React 已 memo、动画用 NativeAnimatedTurboModule

附录

附录 A:关键代码索引

线程与任务调度

模块文件说明
TaskExecutorcpp/RNOH/TaskExecutor/TaskExecutor.h/.cpp4 线程统一调度,:22-23 命名 RNOH_MAIN/RNOH_JS,:29-31 设 QoS
ThreadTaskRunnercpp/RNOH/TaskExecutor/ThreadTaskRunner.cppRNOH_JS 真实 OS 线程 + uv loop,pthread_setname_np
NapiTaskRunnercpp/RNOH/TaskExecutor/NapiTaskRunner.cppRNOH_MAIN 复用宿主 napi/uv loop
EventLoopTaskRunnercpp/RNOH/TaskExecutor/EventLoopTaskRunner.h/.cpp同步/异步/延迟队列,同步优先
MessageQueueThreadcpp/RNOH/MessageQueueThread.h/.cppcxxreact → TaskThread::JS 适配
FFRTConfigcpp/RNOH/FFRTConfig.h并行场景线程上限与 QoS
ParallelCheckcpp/RNOH/ParallelCheck.h/.cpp并行化总开关与设备类型

性能监控

模块文件说明
RNOHMarkercpp/RNOH/Performance/RNOHMarker.h/.cppmarker 总线,~50 个 marker id
PerformanceMetricsRegistrycpp/RNOH/Performance/PerformanceMetricsRegistry.h启动/TTI 指标聚合,getSnapshot
HiTraceRNOHMarkerListenercpp/RNOH/Performance/HiTraceRNOHMarkerListener.cppmarker → HiTrace
NativeTracingcpp/RNOH/Performance/NativeTracing.cppJS Systrace.* → HiTrace
JSFpsMonitorcpp/RNOH/Performance/JSFpsMonitor.cppJS 线程帧命中率
PerfMonitorControllerets/RNOH/PerfMonitorController.ets浮层后端,:85-92 PSS
RNPerfMonitorViewets/RNOH/RNPerfMonitorView.ets浮层 UI,500ms 轮询
UIFpsMonitorets/RNOH/UIFpsMonitor.tsUI FPS(displaySync)
RNOHLoggerets/RNOH/RNOHLogger.tshilog + Tracer,>100ms warn

VSync / 动画 / 定时器

模块文件说明
UITickercpp/RNOH/UITicker.hVSync 心跳,:146-154 前后台,:156-191 tick 暂停逻辑
NativeVsyncHandlecpp/RNOH/NativeVsyncHandle.hOH_NativeVSync RAII
VSyncListenercpp/RNOH/VSyncListener.h/.cpp单次 VSync 请求
EventBeatcpp/RNOH/EventBeat.h/.cppUITicker→RuntimeScheduler 单次桥接
HarmonyTimerRegistrycpp/RNOH/HarmonyTimerRegistry.h/.cppJS setTimeout/setInterval 实现,:213-249 唤醒策略,:77-83 后台暂停
NativeAnimatedTurboModulecpp/RNOHCorePackage/TurboModules/Animated/NativeAnimatedTurboModule.cpp动画驱动,LifecycleObserver 后台 stop

挂载与渲染

模块文件说明
SchedulerDelegatecpp/RNOH/SchedulerDelegate.cpp挂载主路径,:140-162 调优常量,:317-341 FlashList 串行,:376-393 文本切片,:780+ performTransaction
MountingManagerCAPIcpp/RNOH/MountingManagerCAPI.cppCAPI 挂载执行
ComponentInstanceProvidercpp/RNOH/ComponentInstanceProvider.cpp预分配,:171 帧截止保护

内存与冻屏

模块文件说明
RNInstanceInternalcpp/RNOH/RNInstanceInternal.cpp:471-490 onMemoryLevel,:492-508 onBackground GC,:801-840 堆查询
HiCollieFreezeHandlercpp/RNOH/HiCollieFreezeHandler.h/.cpp3s/6s 冻屏抓 JS 栈
FreezeStackCollectorcpp/RNOH/FreezeStackCollector.h/.cppHermes 采样栈采集
ImageCacheMemoryCachecpp/RNOH/ImageCache/src/MemoryCache/ImageCacheMemoryCache.cpp图片缓存,CRITICAL Clear / MODERATE ClearHalf
HermesMemoryWaterMarkcpp/RNOH/JSEngine/hermes/HermesMemoryWaterMark.h/.cppHermes 堆水位

上述路径前缀 cpp/ 指 packages/tester/harmony/react_native_openharmony/src/main/cpp/RNOH/,ets/ 指 packages/tester/harmony/react_native_openharmony/src/main/ets/RNOH/。

附录 B:RNOHMarker 关键 id

启动类:APP_STARTUP_START/STOP、RUN_JS_BUNDLE_START/STOP、INIT_JS_RUNTIME_START/STOP、REACT_INSTANCE_INIT_*、TURBO_MODULE_SETUP_*、CONTENT_APPEARED、DOWNLOAD_*、BUNDLE_SIZE。

Fabric 类:FABRIC_COMMIT_START/END、FABRIC_DIFF_START/END、FABRIC_LAYOUT_START/END、FABRIC_BATCH_EXECUTION_START/END、FABRIC_UPDATE_UI_MAIN_THREAD_START/END、FABRIC_FINISH_TRANSACTION_START/END。

其他:CHANGE_THREAD_PRIORITY、ON_HOST_RESUME_*、ON_HOST_PAUSE_*。

附录 C:编译开关

开关文件作用
WITH_HITRACE_SYSTRACEcpp/CMakeLists.txt:8-10JS Systrace.* → HiTrace
WITH_HITRACE_REACT_MARKERcpp/CMakeLists.txt:8-10RNOHMarker → HiTrace
LOG_VERBOSITY_LEVELcpp/RNOH/CMakeLists.txt:53-57C++ 日志详尽度,release 设 0
PARALLELIZATION_ONParallelCheck.cppFFRT 并行化总开关

附录 D:相关文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐