前作(render-pipeline-compare.pages.dev)从 XNU 源码推演了 iOS/鸿蒙/安卓渲染管线并给出架构判定。本文用鸿蒙真机把这些判定落到测量数据上: 按照之前 iOS 侧的组件树方法论(树 dump / 类型直方图 / 可见性分类 / 双通道交叉验证), 在 Mate 80 Pro 上找到等价测量通道,同时观测到鸿蒙的三棵树——应用进程内的 ArkUI 组件树、 应用进程内的 client RS 节点树、以及 render_service 渲染进程内的 RS 渲染树。
task_conversion_eval 平台墙保护,第三方无法观测。
这是鸿蒙(OpenHarmony 工具链)在可观测性上对 iOS 的反向优势。OtherModifiers 带 pid: 前缀
(SceneBoard 1385 条 / 浏览器 1269 条属性命令),事务队列 transactionFlags[30426, 14696]——
"应用提交命令、RS 进程建树"有了数据级证据,而不只是源码推演。之前 iOS 侧的组件树方法论(M1-M6 指标 × 双通道交叉验证)在鸿蒙上找到如下等价物:
| 指标 | iOS 通道(iPhone 12 Pro / iOS 14.8) | 鸿蒙通道(Mate 80 Pro / HarmonyOS 7) |
|---|---|---|
| M1 树 dump | debugserver + lldb recursiveDescription | hdc shell uitest dumpLayout(ArkUI 可访问性树,JSON) |
| M1′ 树 dump(第二通道) | 无(backboardd 树拿不到) | hidumper -s RenderService -a "client"(应用侧 RS 节点树,带 frameNodeTag) |
| M2 存活计数 | uicount 堆普查(UIView/CALayer 实例数) | hidumper RSTree + nodeNotOnTree(树上/不在树上 RS 节点) |
| M3 类直方图 | ObjC 类名(MM*×N、LOT*×112 框架指纹) | ArkUI 组件类型(Stack/Column/Text…)+ RS 节点类型(CANVAS/SURFACE_NODE…) |
| M4 内存 | phys_footprint / task_vm_info | /proc/<pid>/status VmRSS + allSurfacesMem(surface buffer 总量) |
| M6 可见性 | vis_classify.py 离线分类(递归 window/frame 判定) | bounds 矩形 vs 屏幕矩形(1280×2832)直接判定 |
| 双时间点稳定 | t0/t1 树不变 | t0/t1 dump 节点数一致(315 = 315 ✓) |
通道差异说明:iOS 的 recursiveDescription 是进程内对象树全量(含不可访问节点); 鸿蒙 uitest dumpLayout 是可访问性树(ArkUI 对无障碍可见的节点),口径略窄;但 hidumper 的 client/RSTree 通道 补上了 RS 侧全量。三通道互为印证(组件类型分布三通道高度一致),等价性成立。
| 设备 | HUAWEI Mate 80 Pro(SGT-AL00B),HarmonyOS 7,HongMeng Kernel 1.13.0,aarch64,屏幕 1280×2832 |
| 连接 | hdc(USB),序列号 5SM0125421000216 |
| 测量页面 | 系统浏览器(ei.hmos.browser,pid 30426)首页——信息流页面,功能对标 iOS 侧的 bilibili 首页 |
| 页面状态 | 拉起后静息 10s+,t0/t1 双时间点 dump 验证稳定(315 = 315 节点)✓ |
| 工具链 | uitest dumpLayout / hidumper -s WindowManagerService / hidumper -s RenderService(RSTree、client、nodeNotOnTree、allSurfacesMem)/ ps -ef / /proc/<pid>/status |
| 系统窗口背景 | SceneBoard(pid 59356)托管 20+ 系统窗口(桌面/壁纸/状态栏/导航条),浏览器窗口 browser0 为焦点(ZOrd 102) |
iOS 方法论里"组件树"只有一棵(UIView/CALayer 层级)。鸿蒙真机上同一时刻能观测到三棵语义不同的树:
| 指标 | 数值 | 解读 |
|---|---|---|
| 树节点总数(含根) | 315 | vs iOS bilibili 首页 1,162 / 微信登录页 63 |
| 最大深度 | 59 | vs iOS 典型 10~20 层——ArkUI 的 Navigation/NavDestination/布局容器层层包装 |
| 可见性(M6 等价) | 99.7% 屏内 + 1 个零尺寸 | vs iOS bilibili 首页 15.3% 可见——见 §7 架构解读 |
| 可点击 / 可滚动 | 30 / 4 | 交互面密度 |
| 带资源 id | 71 | 22.5% 节点有显式 id(自动化/测试锚点) |
Stack 54 · Column 52 · Text 44 · Row 39 · __Common__ 15 · SymbolGlyph 13 Flex 11 · Image 10 · GridCol 10 · GridRow 9 · Progress 8 · ListItem 7 (Swiper 4 · CustomFrameNode 4 · Scroll 3 · Navigation 2 · Tabs 2 …)
纯声明式组件指纹:布局容器(Stack/Column/Row/Flex/Grid*)占绝对主体,内容节点(Text/Image/SymbolGlyph)次之。 对应 iOS 侧同类页面直方图里的框架指纹(bili 的 LOT*×112 Lottie 动画层、CAShapeLayer×411)—— 鸿蒙首页没有"隐藏动画层囤积"的痕迹,与其 99.7% 可见率互相印证。
这是 iOS 侧完全不存在的观测面:RenderService 视角下应用进程自己持有的 RS 节点代理树, 每个节点带 ArkUI 帧节点映射(frameNodeId / frameNodeTag):
| pid[30426] transactionFlags:[ 30426, 27762]ImplicitCommonTransactionData, [Command: (Node:131657927491585, Type:3, SubType:26) ...] | SurfaceNode[...], name[browser0], isUifirstNode[true], RSCmdModifiers: [type=ABILITY_BG_ALPHA...] | RootNode[...], frameNodeId[0], frameNodeTag[root], modifiers[Bounds: 1280x2832...] | CanvasNode[...], frameNodeId[1], frameNodeTag[stage] | CanvasNode[...], frameNodeId[2], frameNodeTag[page] | CanvasNode[...], frameNodeId[7], frameNodeTag[NavBar] | CanvasNode[...], frameNodeId[135], frameNodeTag[NavBar] ... | CanvasNode[...], frameNodeTag[Stack], RSCmdModifiers: [type=IS_REPAINT_BOUNDARY...]
| 统计项 | 数值 | 解读 |
|---|---|---|
| client 树节点总数 | 458(450 CanvasNode + 5 SurfaceNode + 3 RootNode) | 同一页面:ArkUI 可访问性树 315 < client RS 树 458——RS 侧节点多于可访问性可见节点 |
| frameNodeTag 分布 | Text 79 · Column 64 · Row 54 · Stack 44 · Image 24 … | 与 uitest 类型直方图同构(Text 44↔79、Column 52↔64、Stack 54↔44)——三通道互证 |
| 事务队列 | transactionFlags [30426, 27762/27763] ImplicitCommonTransactionData | 应用在持续提交隐式事务(页面有动画/刷新) |
| 特性标记 | isUifirstNode[true]、isSingleFrameComposer | HarmonyOS 7 的 UIFirst/单帧合成优化路径已启用 |
frameNodeTag 是黄金证据:ArkUI 组件树的每个帧节点(FrameNode)到 RS CanvasNode 的映射关系在 client dump 里直接可见——"组件树 → 渲染节点树"的转换不是黑盒。
这是前作架构判定的直接验证目标——渲染树到底建在哪个进程:
| RS_NODE[0] // 渲染树根(进程 pid 1788 内!) | SCREEN_NODE[...], screenId[0], Frame[0 0 1280 2832] | LOGICAL_DISPLAY_NODE[...], nodeName[SCBScreen-Temp] | CANVAS_NODE[...], nodeName[OneHandModeBox] | CANVAS_NODE[...], nodeName[transformSceneSCBWallpaper6] // 每窗口一个场景容器 | SURFACE_NODE[...], Name[SCBWallpaper6] // 壁纸 surface | CANVAS_NODE[...], nodeName[transformSceneSCBDesktop8] | SURFACE_NODE[...], Name[SCBDesktop8] // 桌面 surface | CANVAS_NODE[...], nodeName[home], OtherModifiers:[pid:59356->Bounds...pid:30426->...] | SURFACE_NODE[...], Name[browser0 / SCBStatusBar17 …] // 窗口 surfaces Animating Node: [130678674949881, 130678674949863] // RS 侧驱动的动画节点 -- RS transactionFlags: [30426, 14696] // 浏览器的事务正在队列中
| 统计项 | 数值 | 解读 |
|---|---|---|
| 渲染树节点总数 | 668 | 全部构建于 render_service 进程(pid 1788)——不在任何应用进程 |
| 节点类型 | CANVAS_NODE 650 · SURFACE_NODE 10 · ROOT 3 · SCREEN/LOGICAL_DISPLAY/EFFECT/UNION 各 1 | 系统级类型学:屏幕→逻辑显示→窗口场景→surface,与 ArkUI 组件类型完全异构 |
| 跨进程命令痕迹 | pid 59356(SceneBoard)1385 条 · pid 30426(浏览器)1269 条 · pid 60258(卡片渲染)86 条 | OtherModifiers 里带提交者 pid 的属性命令——应用→RS 命令流的数据级证据 |
| 动画 | Animating Node ×2 | 动画节点挂在 RS 树上由渲染进程驱动(对标 iOS 的 server 端插值) |
| 效果节点 | EFFECT_NODE(FrostedGlassBlur 等) | 背景模糊等系统效果在 RS 树上以节点形式存在 |
架构判定验证:668 个渲染树节点、10 个窗口 surface、跨进程命令痕迹、RS 侧动画节点—— 全部观测于 render_service 渲染进程(pid 1788)内部。前作"渲染树由 RS 渲染进程构建并持有"的论断, 从源码推演升级为真机数据实锤。
RS 渲染树的 dump 不止给出结构,还给出谁在提交命令:
pid:NNN-> 前缀。
同屏状态下 SceneBoard(59356)提交了 1385 条属性命令、浏览器(30426)1269 条、
桌面卡片渲染服务(60258)86 条——多应用并行向同一棵 RS 树提交命令的直接观测;RS transactionFlags: [30426, 14696]——RS 主线程待处理的浏览器事务序号;
client 通道还捕获到 ImplicitCommonTransactionData 与具体 Command 列表
(Node:id, Type:1, SubType:272/288);RSCmdModifiers(如 ABILITY_BG_ALPHA、IS_REPAINT_BOUNDARY)
对应源码中 RSCanvasNodeCommandHelper::UpdateRecording 录制的 DrawCmdList(见 §6 源码)。这条证据链与 iOS 侧形成有趣对照:iOS 的 CA 事务走 Mach IPC(内核侧我们锚定了
mach_msg_overwrite_trap),但 backboardd 内部对第三方不可见;鸿蒙的 RS 事务流全程可 dump。
| 对象 | 实测值 | 解读 |
|---|---|---|
| render_service 进程 | VmRSS 164.7 MB(匿名 91.2MB,46 线程) | 系统渲染进程的常驻成本——全系统唯一的合成者 |
| 浏览器进程 | VmRSS 741.3 MB(匿名 392MB,124 线程) | 应用进程只管组件树与内容生产 |
| 全系统 surface buffer | 23.9 MiB(allSurfacesMem) | 显式缓冲池——对标 iOS 的 IOSurface/BufferQueue 口径 |
| 浏览器 WebView surfaces | RosenWeb / RosenWebPopup BufferQueue(FIFO=5) | 网页内容不走 ArkUI 树,以 surface 形式挂载(对标 iOS WKWebView 的 RemoteLayerTree) |
| 不在 RS 树上的节点 | nodeNotOnTree:WebView 的 surface 节点若干 | 鸿蒙版"存活 vs 上树"指标——RS NodeMap 里注册但未挂到主树的节点(对标 iOS M1/M2 比值语义) |
iOS 方法论里最核心的 M1/M2 比值(上树/存活:微信 0.14、bili 0.90)在鸿蒙上有了原生对应物:
RenderService 的 NodeMap 本身维护"注册/上树"两态(源码 RegisterRenderNode /
UnRegisterUnTreeNode,见 §6),hidumper nodeNotOnTree 直接输出差集。
实测浏览器场景的差集主要是 WebView 双缓冲 surface——语义上与 iOS 的"离屏 layer 存活但不渲染"一致。
以下锚点基于 OpenHarmony 开源对应物(HarmonyOS 7 商业版闭源,但 Rosen 图形栈与开源同源; 真机 dump 的符号名与开源实现一一对应):
if (context.GetNodeMap().GetNodeCountByPid(ExtractPid(id)) > MAX_NODE_COUNT_PER_PID) { RS_LOGE_LIMIT(... "GetNodeCountByPid > %u, pid:%u", MAX_NODE_COUNT_PER_PID ...); return; // ← 每应用渲染节点数有上限(防泄漏) } auto node = RSRenderNodeAllocator::Instance().CreateRSCanvasRenderNode(id, ...); if (context.GetMutableNodeMap().UnRegisterUnTreeNode(id)) { ... } // ← “不在树上”注册表(nodeNotOnTree 的实现基础) context.GetMutableNodeMap().RegisterRenderNode(node); // ← RS 进程内从命令构建节点并注册
这段就是 §3.3 实测的源头:应用发来的 Create 命令在 RS 进程内实例化 RSCanvasRenderNode、
注册进 NodeMap;每 pid 节点数受 MAX_NODE_COUNT_PER_PID 保护;
Register/UnRegisterUnTreeNode 双注册表正是 nodeNotOnTree 观测的实现基础。
// RS 渲染主线程(真机 pid 1788 的主循环): ProcessCommand(); // L572 — 消化各应用提交的事务命令(本帧 668 节点树的输入) Render(); // L591 — “now render is traverse tree to prepare” 遍历树准备渲染 // ProcessCommand() 内部: if (isUniRender_) ProcessCommandForUniRender(); // 统一渲染 / 分离渲染两条路径 else ProcessCommandForDividedRender(); switch (context_->purgeType_) { // ← RS 侧内存回收(对标 iOS purgeable) case RSContext::PurgeType::GENTLY: ClearMemoryCache(...); break; case RSContext::PurgeType::STRONGLY: ClearMemoryCache(..., true); break; }
RS 主线程的循环形状与 iOS render server 的职责对齐:消化命令 → 遍历树 → 动画插值 → 合成;
PurgeType::GENTLY/STRONGLY 的两级缓存回收,语义上对标 iOS 侧 purgeable volatile 的
"压力下丢弃可重建内容"。
if (gDumpNodeTreeProcessor) { gDumpNodeTreeProcessor(nodeId, pid, token, taskId); // ← hidumper client/RSTree dump 的实现钩子 }
本文所有树 dump 的合法性也锚在源码上:NodeMap 层预留的 DumpNodeTreeProcessor 回调, 让系统诊断工具能按 pid/token 输出节点树——iOS 把同样的信息锁在平台墙内,OpenHarmony 把它做成了公开工具接口。
| 维度 | 鸿蒙(本次真机实测) | iOS(前作基线,iPhone 12 Pro / 14.8) |
|---|---|---|
| 组件树语义 | ArkUI 声明式:树 = 当前状态推导结果,if/LazyForEach 按需实例化 | UIKit 命令式:树 = 累积构建结果,复用池/预构建导致存活 ≫ 上树 |
| 首页级树规模 | 浏览器首页 315 节点(信息流) | bilibili 首页 1,162 节点 / 微信登录页 63 |
| 树深度 | 59 层(Navigation/容器层层包装) | 典型 10~20 层 |
| 可见率 | 99.7% 屏内(314/315) | bili 首页 15.3%(177/1,162) |
| 渲染树归属 | render_service 进程(668 节点实测于 pid 1788)——树的本尊在渲染进程 | 应用进程内构建 layer 树,server 侧为提交镜像(backboardd 不可观测) |
| 应用→渲染传输 | 命令/属性流(OtherModifiers pid 痕迹 + transactionFlags 可直接 dump) | CA 事务经 Mach IPC(mach_msg_overwrite_trap,内核侧可锚定;server 侧不可见) |
| 动画驱动 | RS 渲染进程(Animating Node 实测挂 RS 树上) | render server 端插值(架构推断,进程内不可直接观测) |
| "存活 vs 上树"指标 | NodeMap 双注册表 + nodeNotOnTree 工具(原生支持) | M1/M2 双通道测量(lldb 树 + 堆普查,需自建工具) |
| 离屏内容回收 | RS 侧 PurgeType 两级回收 + nodeNotOnTree 差集 | purgeable/volatile(内核 token 状态机)+ WebKit volatile 标记 |
| WebView 内容 | 独立 RosenWeb surface(FIFO=5 BufferQueue),不进 ArkUI 树 | WKWebView RemoteLayerTree(独立 WebContent 进程) |
| 可观测性 | 三棵树 + 命令流 + surface 内存全部可 dump(hidumper/uitest) | 应用侧树可 dump(lldb);server 侧被平台墙保护(task_conversion_eval) |
可见率鸿沟的架构解读:99.7% vs 15.3% 不是"谁更好",而是两种树哲学的必然结果—— ArkUI 的树是状态推导的瞬时快照(不可见分支根本不实例化,LazyForEach 只构建视口内条目), UIKit 的树是历史累积的对象图(复用池、预布局、屏幕外 layer 全部存活)。 这也解释了 iOS 侧 M1/M2 比值能低到 0.14(微信),而鸿蒙侧差集主要是 WebView surface 这种结构性双轨。
一句话总结:在鸿蒙真机上,iOS 方法论的每个指标都找到了等价甚至更强的通道—— 更重要的是,前作从源码推演的"应用内组件树 → 命令流 → RS 渲染进程建树渲染"模型, 现在有了进程级(pid 1788)、树级(668 节点)、命令级(OtherModifiers pid 痕迹)三层实测证据。 iOS 把渲染树的观测锁在平台墙内,鸿蒙把它做成了公开诊断接口——这是两平台工程文化的真实差异。
证据边界声明:[EXP] 全部来自 2026-09-01 的 Mate 80 Pro(HarmonyOS 7)真机, 页面为系统浏览器首页静息态;与 iOS 基线(bilibili 首页 / 微信登录页,iPhone 12 Pro / iOS 14.8.1)为 不同 App 的功能对标而非同一 App——量级对比仅供参考,可见率与树哲学的差异为架构级结论。 [SRC] 锚定 OpenHarmony 开源对应物(master 分支),HarmonyOS 7 商业版闭源但真机 dump 的符号名 (RSCanvasNodeCommandHelper / RSMainThread / NodeMap)与开源实现一一对应。 iOS 侧数据引用自前作与 ios-uicount 方法论基线(bili/微信实测,iPhone 12 Pro / iOS 14.8.1)。