iOS 页面组件计数分析 M1 视图树 · M2 堆普查 · M5 组件内存 · M6 可见性 · 交叉验证

📐 渲染管线 × 图片内存变迁全景示意图(SVG)→

节点详情

点击左侧任意节点查看详情

弧长权重:当前「节点数

环 = 层级深度(内→外)· 弧长 = 所选度量的子树规模 · 颜色 = 来源(■系统 ■应用私有 ■三方库 ■独立图层,越深越暗)· 对象内存 = class_ro_t.instance_size 设备实测(view+layer 真实壳)· 背衬估算 = contents 实测位图优先(bilibili 66 层)+ 自绘类启发式(容器类无 backing)· 可见节点 = M6 静态可见性判定(hidden 传播/裁剪/离屏/遮挡启发式,假设恒等 transform)· 悬停看子树规模,点击查节点详情

节点详情

点击旭日图任意环段

读图要点

· 一整圈就是一个 window 的渲染树,从根(中心)到叶子;
· 厚弧大段 = 该分支子树庞大(如 bilibili 的 feed 卡片区);
· 细碎密齿 = 大量同类兄弟组件(如 CAShapeLayer 装饰层);
· 颜色随深度变暗,可快速目测「系统壳有多厚、业务层埋多深」;
· 中心数字 = 全树节点总数。
拖拽平移 · 滚轮缩放(以光标为焦点)· 点击节点折叠/展开(火焰图点击缩放)

树上组件 · 按类计数

全部 系统 UIKit 应用私有 三方库

堆普查 · 存活实例类直方图

通道 C(uicount 全 malloc 区扫描)· 与左图同页面对照:树=在屏,堆=存活

类分布矩形树图

面积 = 所选度量 · 颜色 = 来源 · Top 60 类(悬停看计数与内存)

树深度分布

来源构成

树上节点按实现来源分类

视图背衬图层(view → backing layer)

每个 UIView 子类实例由哪个 CALayer 子类承载渲染

独立图层节点(sublayer,不在 view 树)

recursiveDescription 下钻 layer.sublayers 的产物(iOS 13+)· 渲染树的一部分

视图 ↔ 图层 汇总

malloc 分区分布(对象在哪个区)

通道 C 按 VM tag 扫描 · tag10=medium 区是 UIView 子类主居所(>255B)
布局 × 热力 · 按真实 frame 定位

悬停查看详情

微信登录页 vs bilibili 首页 · 指标对照

同设备同协议(静息态、挂起瞬间双通道)· 条形为等比缩放

数值表

M1/M2 比值解读:M1(树)= 此刻渲染什么;M2(堆)= 此刻存活什么。 bilibili 首页 0.90 —— 树与堆基本一致,按需创建、用完即弃; 微信登录页 0.14 —— 444 个存活 view 仅 63 个上树,其余为预构建缓存(WCRedesign* 表单组件、MMUIButton 等),激进预构建风格的实证。 该比值本身即页面实现风格指标,可直接用于跨框架(如 ArkUI)对比。

iOS vs 鸿蒙 · 图片管线与解码时机对比

iOS 侧为挂起态实测(iPhone 12 Pro · iOS 14.8.1 · 四点曲线实验)· 鸿蒙侧为文档级推断(无设备 · 待实测)——两侧结论强度已分别标注
核心结论:两套管线在同一条「内存占用 vs 就绪延迟」的权衡曲线上取了不同的点—— iOS 默认惰性(display 阶段才解码 · 解码缓存 purgeable 由系统管压力), 鸿蒙默认急切(createPixelMap 即解码 · LRU 缓存由应用管生命周期)。 各有胜负场景,现有证据不足以得出单边「谁更优」——差距恰好落在 iOS 平台墙盲区与鸿蒙未实测两个区。

iOS · ImageIO 路径 (实测 ✓)

① 加载
压缩 0.86MB
App 堆 ✓footprint

display 首绘
② 解码
位图 48MB
App 堆 ✓footprint
+缓存
48MB
purgeable ✗不计

commit 上传
③ server
纹理 48MB
✗ App 账 🔒平台墙
惰性红利:隐藏组件只有压缩数据(tag-88 全程 0 实证)· 代价:首次上屏主线程解码 → 大图掉帧。ImageIO 缓存 volatile:内存压力下系统静默回收不杀进程(internal+96MB vs footprint+48MB 的双缓冲巧账)

鸿蒙 · ArkUI 路径 (文档级 ⏳)

① 组件创建
ImageSource
编码态

createPixelMap
(eager)
② 解码
PixelMap 48MB
DMA / 堆

RS 渲染
③ 零拷贝
dma-buf 导入
全系统一份
+缓存
LRU 256MB
持久 · 默认
急切红利:常显图 DMA 一份全系统共享、首帧就绪不卡 · 代价:隐藏组件默认也吃 48MB 解码内存,直到 LRU 逐出。零拷贝依赖硬解+DMA 路径(软解/部分格式可能仍是堆 PixelMap + 上传——设计上限 ≠ 默认现实,待实测)

工作负载 × 胜者

负载iOS(lazy + purgeable)鸿蒙(eager + DMA + LRU)胜者
常显示、反复用的图堆位图 + server 纹理 = 系统两份DMA 一份全系统共享 · 缓存命中不重解码鸿蒙
大量隐藏/预建图
(bili 首页 63.2% 隐藏类负载)
仅压缩数据 0.86MB/张默认解码 48MB × N,直到 LRU 上限iOS(量级差)
内存压力行为purgeable 静默回收 · 不杀进程LRU 逐出 · 256MB 内持久占用,靠应用调参iOS
首帧延迟首次绘制主线程解码 → 大图掉帧已就绪 → 流畅鸿蒙

对「鸿蒙管线更优」论的前提检验(三处修正)

#原论证检验结果
修正 ①「鸿蒙支持 lazy 可选,所以更优」不成立——两端对称:iOS 可 predecode 转 eager(iOS 15+ preparingForDisplay),鸿蒙可 DIY 转 lazy。差别只是默认值方向,不是能力
修正 ②「iOS 需从 ImageIO 拷贝到 IOSurface」机制说反半步——拷贝是「App 堆 → server 纹理」的一次性上传(server 缓存复用,非每帧);且 iOS 视频路径(CVPixelBuffer)/Metal 本就零拷贝,「必须拷贝」只对静态图成立。实测:解码产物 tag-88 全程 0,从头到尾不是 IOSurface
修正 ③「PixelMap 走 DMA 零拷贝」设计上限 ≠ 默认现实——硬解路径落 DMA,软解/部分格式仍可能堆 PixelMap + 上传;GPU 采样 dma-buf 还受格式/stride 兼容约束(文档级,待实测)

测量盲区与判别实验(A–E 组)

盲区声明:iOS server 纹理在平台墙后(XNU task_conversion_eval ipc_tt.c:2183)——「全系统总量」对比在 iOS 侧只能推算不能实测;鸿蒙侧当前全部为文档级推断。
配置验证什么状态
A鸿蒙默认 Image + 隐藏(Visibility.None)eager 基线:隐藏态是否就吃 48MB 解码内存(hidumper --mem 查 PSS/dma_heap)⏳ 待设备
B鸿蒙 LazyForEach 屏外 item组件级懒的边界:组件不创建是否即不解码⏳ 待设备
C鸿蒙 DIY:持 ImageSource + 可见才 createPixelMap应用级懒 = iOS 语义复刻(四点法直接套用)⏳ 待设备
DiOS 默认(隐藏图片组件)lazy 基线:隐藏态只有压缩数据✓ 已测(tag-88=0 · 压缩 0.86MB)
EiOS predecode(offscreen 强制绘制)eager 形态复刻:验证「两端默认值不同、能力对等」⏳ 可立即做
判定标准:若 A 组证实隐藏即解码、E 组证实 iOS 可复刻 eager → 结论「默认值方向不同、各有胜负场景」成立;若鸿蒙默认在全负载不差且 C 组最优配置 ≤ iOS → 才能写「管线设计占优」;只要隐藏预建负载鸿蒙显著更差,「鸿蒙更优」就必须反写。
延伸:📐 渲染管线 × 图片内存变迁全景图(SVG) · iOS 侧四点曲线实验设计 raw-data/exp-decode-2026-09-24.md · 解码触发时点 = CA 周期 layout → display(解码在此) → commit(运输)