TwinShelf 做到 05,已经不缺新的“分栏玩法”了。

现在同一个阅读 Demo 已经跑过:

Navigation Auto / Split
默认详情
1:2 / 2:1 比例
右栏连续路由
Transition Page
ImageGallery Fullscreen
System Bar 恢复
Landscape
reader_workspace
Chart / Notes / Discussion
In-App Split
compare_workspace
Secondary Route Restore

最后一篇如果继续加一个新页面,只会把工程线拉得更散。

所以 06 固定做一件事:

把前五篇形成的状态协议放进同一套回归矩阵,重复执行,证明它们不只是“单次 Demo 能跑”,而是可以持续迭代。

当前官方 HarmonyOS 7 平行视界能力已经支持更多分栏比例,官方示例也覆盖主页关联、路由模式、过渡页、全屏、横屏、虚拟容器和应用内分屏等典型场景;Navigation 本身则继续提供 Stack / Split / Auto、NavPathStack、navBarWidthRange、splitPlaceholder 等基础能力。

TwinShelf 的最终回归就围绕这些已经真实实现过的主线展开。

本轮统一数据:

taskId:
parallel_accept_20261003_06

profiles:
5

cycles:
6

totalRuns:
30

passed:
30 / 30

routeMismatch:
0

selectionMismatch:
0

scrollMismatch:
0

ratioMismatch:
0

fullscreenRestoreMismatch:
0

orientationRestoreMismatch:
0

virtualContainerMismatch:
0

inAppSplitMismatch:
0

contentRebuildMismatch:
0

windowStateLeak:
0

activeSubNavStacksAfterFinish:
0

activeFullscreenRequestsAfterFinish:
0

snapshotsReleased:
30

avgRoutePush:
2.4ms

p95RoutePush:
4.2ms

avgRatioSwitch:
3.2ms

p95RatioSwitch:
5.1ms

avgFullscreenTransition:
7.1ms

p95FullscreenTransition:
10.8ms

avgVirtualSwitch:
2.8ms

p95VirtualSwitch:
4.6ms

avgInAppSplitOpen:
5.5ms

p95InAppSplitOpen:
8.7ms

baselineMemory:
142.8MB

after6Cycles:
144.0MB

memoryDelta:
+1.2MB

maxMemory:
196.7MB

status:
PASS

一、五个 Profile 对应前五篇真实工程主线

最终矩阵不是随机找五个窗口。

固定 Profile:

DEFAULT_SPLIT
1280×800vp
默认分栏与文章选中

RATIO_ROUTE
1440×900vp
1:2 / 2:1 与右栏路由

FULLSCREEN_MEDIA
1440×900vp
Transition + Gallery + SystemBar

VIRTUAL_WORKSPACE
1440×900vp
Landscape + reader_workspace

IN_APP_SPLIT
1600×900vp
compare_workspace + Secondary Route

每个跑:

6 轮

总计:

5 × 6 = 30

最终:

30 / 30

二、回归必须比较“状态结果”,不能只看页面有没有崩

如果某一轮:

article_042
最后还是显示出来了

不代表这轮一定正确。

中间可能发生:

scroll 归零;
右栏多一层幽灵路由;
全屏系统栏没恢复;
Notes 草稿被重建;
Secondary Route 没释放。

所以 TwinShelf 每个 Profile 都生成:

Acceptance Snapshot

然后按字段比较。

三、第一段代码把五类 Profile 写成固定配置

interface RegressionProfile {
  name: string

  widthVp: number
  heightVp: number

  fullscreen:
    boolean

  virtualWorkspace:
    boolean

  inAppSplit:
    boolean
}

const PROFILES:
  RegressionProfile[] = [
    {
      name:
        'DEFAULT_SPLIT',
      widthVp:
        1280,
      heightVp:
        800,
      fullscreen:
        false,
      virtualWorkspace:
        false,
      inAppSplit:
        false
    },
    {
      name:
        'RATIO_ROUTE',
      widthVp:
        1440,
      heightVp:
        900,
      fullscreen:
        false,
      virtualWorkspace:
        false,
      inAppSplit:
        false
    },
    {
      name:
        'FULLSCREEN_MEDIA',
      widthVp:
        1440,
      heightVp:
        900,
      fullscreen:
        true,
      virtualWorkspace:
        false,
      inAppSplit:
        false
    },
    {
      name:
        'VIRTUAL_WORKSPACE',
      widthVp:
        1440,
      heightVp:
        900,
      fullscreen:
        false,
      virtualWorkspace:
        true,
      inAppSplit:
        false
    },
    {
      name:
        'IN_APP_SPLIT',
      widthVp:
        1600,
      heightVp:
        900,
      fullscreen:
        false,
      virtualWorkspace:
        false,
      inAppSplit:
        true
    }
  ]

这份配置下一版本不随意改。

只有产品策略真正变化,才新建新的 Baseline Version。

四、Route Mismatch=0 不只检查栈深度

路由一致性至少检查:

当前 top route
route params
stack depth
duplicate route count
restore target

例如 FULLSCREEN_MEDIA:

1→2→3→2→1

最终回 article_042。

如果深度是 1,但 top route 不是 article_042,仍然失败。

本轮:

routeMismatch=0

五、选中态与滚动位置独立统计

当前:

selectionMismatch=0
scrollMismatch=0

它们不能合并成:

pageStateMismatch

因为问题定位完全不同。

选择丢失通常来自:

primary / secondary Store 混用

滚动位置丢失则更可能来自:

页面重建
Snapshot Owner 错误
恢复时机不对

拆开以后,回归报告更容易指导排查。

六、比例回归不仅检查最后是不是 1:2

RATIO_ROUTE 本轮序列:

1:2
→ 2:1
→ 1:2

回归检查:

ratioChanges
route state
left selection
right route
final ratio

如果比例切对了,但右栏路由栈被重建:

仍然 FAIL

本轮:

ratioMismatch=0

七、全屏恢复必须同时看 Window 和 Navigation

FULLSCREEN_MEDIA 检查:

ImageGallery 是否真正进入;
System Bar 是否隐藏;
System Bar 是否恢复;
Window Layout 是否恢复;
Route Depth 是否恢复;
Article Scroll 是否保持;

当前:

fullscreenRestoreMismatch=0

不能只看:

Gallery pop 了。

全屏本身是 Window + Route 的联合状态。

八、横屏恢复不等于“方向最后是竖屏”

04 的 Workspace 会请求:

LANDSCAPE

退出以后恢复:

进入前 Orientation Snapshot

所以回归检查的是:

是否恢复原状态

而不是:

最终一定 PORTRAIT。

当前:

orientationRestoreMismatch=0

九、Virtual Container 回归检查内部状态和外层栈隔离

VIRTUAL_WORKSPACE 每轮都走:

Chart
→ Notes
→ Discussion
→ Notes
→ Chart

检查:

outerDepth=2
notesDraft=86
discussionScroll=344
chartRetained=true
articleScroll=612

最终:

virtualContainerMismatch=0

内部页面再复杂,也不能污染外层 Navigation。

十、In-App Split 必须同时检查主区和对照区

第五个 Profile:

article_042
+
compare_workspace

检查:

primary scroll=612

secondary article=article_118

quote=quote_17

secondary scroll=288

close / reopen restore

最终:

inAppSplitMismatch=0

只验证“分屏打开了”远远不够。

十一、第二段代码把核心状态变成硬断言

export class RouteConsistencyAssert {
  verify(
    expected:
      TwinShelfSnapshot,
    actual:
      TwinShelfSnapshot
  ): boolean {
    return (
      expected.articleId ===
        actual.articleId &&
      expected.routeDepth ===
        actual.routeDepth &&
      expected.scrollOffsetVp ===
        actual.scrollOffsetVp &&
      expected.ratio ===
        actual.ratio &&
      expected.contentRebuildCount ===
        actual.contentRebuildCount
    )
  }
}

复杂 Profile 会在这层基础上增加:

fullscreen
workspace
secondary split

自己的断言。

十二、contentRebuildMismatch=0 是整个系列最重要的一条线

从 01 到 05,一直反复强调:

形态变化
不等于内容重建。

所以 06 最终专门统计:

contentRebuildMismatch=0

哪怕最后状态看起来恢复正确,只要某一轮:

ArticleDetail 被销毁重建

也会记 mismatch。

这能防止“靠重建再恢复数据”伪装成真正的状态连续。

十三、Window State Leak 也必须进入最终资源检查

这个系列操作过很多 Window 状态:

Fullscreen
System Bar
Orientation
Split

如果某轮结束以后:

仍然 fullscreen=true

或者:

Orientation 还停在强制 LANDSCAPE

下一轮就会被污染。

所以最终:

windowStateLeak=0
activeFullscreenRequestsAfterFinish=0

所有 Window 临时状态必须归零。

十四、Secondary / Internal Route Store 也要释放

05 新增 SecondaryRouteStore。

04 有 ReaderWorkspaceStore。

如果 Profile 结束以后还留着:

activeSubNavStacks

说明内部路由生命周期没有收口。

最终:

activeSubNavStacksAfterFinish=0

十五、Snapshots 30 次必须全部释放

每个 Profile 每轮都会创建:

Route Snapshot
Window Snapshot
Workspace Snapshot
Split Snapshot

不一定每轮都有全部类型,但总的 Acceptance Snapshot:

created=30
released=30

所以:

snapshotsReleased=30

和 totalRuns 完全对账。

十六、性能只做同环境纵向基线

本轮项目侧统计:

Route Push
avg 2.4ms
p95 4.2ms

Ratio Switch
avg 3.2ms
p95 5.1ms

Fullscreen Transition
avg 7.1ms
p95 10.8ms

Virtual Switch
avg 2.8ms
p95 4.6ms

In-App Split Open
avg 5.5ms
p95 8.7ms

这些数字不是系统规格。

也不用于和别的应用横向比较。

它们只用于:

同设备
同 Profile
下一版 TwinShelf

回归。

十七、内存看六轮结束后有没有阶梯增长

本轮:

baseline:
142.8MB

after6Cycles:
144.0MB

delta:
+1.2MB

max:
196.7MB

峰值来自:

Gallery
Virtual Workspace
In-App Split

这些同时存在大图、双侧工作区和多个状态 Store 的场景。

真正 Gate 关注:

每轮结束后
稳定基线有没有持续抬高。

当前没有明显阶梯增长。

十八、DevEco 图只保留最终矩阵与失败定位入口

开发图:

左侧:

RouteCoordinator
FullscreenCoordinator
InAppSplitCoordinator
ReaderWorkspaceStore
RegressionRunner

中间:

5 Profiles × 6 cycles

右侧:

30 / 30 PASS

底部:

all mismatch=0

memory:
142.8→144.0

RESULT PASS

如果某一轮失败,Runner 会额外保存:

profile
cycle
routeDepth
articleId
scroll
ratio
fullscreen
orientation
workspace
secondaryRoute

让问题可以复现。

十九、手机运行图最终只回答一件事:五条主线能不能一起稳定

最终运行图:

五个 Profile:

DEFAULT_SPLIT
PASS

RATIO_ROUTE
PASS

FULLSCREEN_MEDIA
PASS

VIRTUAL_WORKSPACE
PASS

IN_APP_SPLIT
PASS

一致性:

全部 0

资源:

SubNavStack=0
FullscreenRequest=0
SnapshotsReleased=30

最终:

PASS

二十、30/30 不代表所有大屏场景都已经覆盖

这个边界必须明确。

PASS 代表:

当前 TwinShelf 版本
当前五个 Profile
当前六轮
当前测试设备与窗口尺寸
当前文章 / 媒体 / Workspace 数据

全部符合预期。

它不代表:

任意窗口
任意语言
任意深链
任意大屏设备

都一定没有问题。

工程回归的价值是“可重复”和“可比较”,不是制造一个绝对结论。

二十一、TwinShelf 六篇真正形成的是一条“路由状态连续”主线

回头看整个系列:

01
单双栏变化
但文章选中不丢

02
比例和右栏路由变化
但左栏选择不丢

03
进入全屏
但文章阅读现场不丢

04
进入虚拟工作区
但外层 Navigation 不乱

05
打开应用内分屏
但主区与对照区状态各自稳定

06
把所有状态协议重复跑 30 次

这个系列真正解决的不是:

怎么让页面看起来像大屏。

而是:

当一个应用在大屏里同时拥有多种窗口、路由和工作区形态时,怎样让业务状态一直有清晰 Owner。

二十二、TwinShelf 到 06 正式结束

系列固定 X=6,到这里完成。

继续写 07,大概率只会变成:

再加一个比例
再加一个内页
再加一个全屏页

新的主矛盾已经不在这里。

下一轮应该切换到明显不同的新方向和新 Demo,从新的 01 开始。

优先可以考虑:

精准碰一碰 / 跨设备投递

闪控窗 / Float View

HarmonyOS 应用上架审核

不再继续扩写 TwinShelf。

二十三、最终回归还要固定“主文章身份”

五个 Profile 虽然分别测试不同能力,但共同基线始终是:

primaryArticleId=article_042

这样做的价值是减少变量。

如果 DEFAULT_SPLIT 测 article_042,FULLSCREEN_MEDIA 改 article_100,IN_APP_SPLIT 又改 article_300,一旦状态不一致,很难判断是场景实现问题还是数据内容差异。

所以 TwinShelf 的 Acceptance Dataset 把文章、媒体、引用、笔记都版本化:

article_042
img_nav_03
article_118
quote_17
notesDraft=86

每轮开始前先校验 Fixture。

二十四、右栏和子工作区的幽灵栈要单独扫描

只看主 NavPathStack:

depth 正常

不代表内部路由一定干净。

04 有 Workspace 内部页面状态。

05 又增加 SecondaryRouteStore。

所以 Resource Probe 会同时扫描:

outer NavPathStack
workspace internal history
secondary route stack
fullscreen transient route

Profile 完成以后要求:

只保留当前场景预期的根状态;
所有临时子栈都回收。

最终:

activeSubNavStacksAfterFinish=0

二十五、System Bar 与 Orientation 需要做“进入前 / 退出后”成对快照

全屏和横屏都属于 Window 状态。

回归开始时先保存:

layoutFullScreen
systemBarEnabled
preferredOrientation

场景结束后重新读取真实 Window 状态。

不是简单相信:

restore() 已经调用。

只有:

before == after

才算通过。

这样能抓住一种很隐蔽的问题:

restore API 调了;
但因为异常分支没有 await;
下一轮开始时状态仍未恢复。

本轮:

windowStateLeak=0

二十六、应用内分屏的 Restore 不能依赖执行顺序“刚好正确”

30 次回归会重复:

open
secondary route
close
reopen
close

如果 Snapshot 是一个全局可变对象,很容易上一轮数据污染下一轮。

每一轮 Profile 开始前:

create generation
clear transient runtime state
load immutable fixture

结束后:

release generation
assert no active secondary context

只有明确需要跨“同一轮关闭 / 再打开”的 Snapshot 才在本轮内保存。

这让六轮之间真正独立。

二十七、性能采样要排除首轮预热

第一次进入 Gallery 或 Workspace 时,可能包含:

模块初始化
图片缓存建立
Builder 首次创建

所以 TwinShelf 的性能 Baseline 使用:

Warm-up run
+
正式 6 轮统计

封面和运行图展示的:

avg / p95

来自正式采样窗口。

首轮预热仍然保存日志,但不进入性能分位数。

正确性断言则每轮都执行,不会因为预热而跳过。

二十八、内存的 +1.2MB 不能直接写成“绝对没有泄漏”

本轮:

142.8MB
→ 144.0MB

整体稳定。

但 UI 图片缓存、系统渲染缓存、GC 时机都可能带来小幅变化。

所以 Resource Gate 不使用:

memoryDelta 必须等于 0

而是结合:

对象计数
临时路由计数
Window 请求计数
多轮内存趋势

一起判断。

如果临时对象全部归零,内存没有阶梯增长,才认为当前回归通过。

二十九、失败报告要保存“状态链”而不是只保存截图

假设第 18 次运行出现:

FULLSCREEN_MEDIA
scrollMismatch

Runner 会保存:

profile
cycle
articleId
startScroll
routeDepthSequence
fullscreenStateSequence
systemBarSequence
endScroll
contentRebuildCount

开发者可以按这条链重放。

对于平行视界,多状态往往是:

前面某一步没有收口
→ 后面某一步才表现异常。

只有状态序列才能真正定位。

三十、回归矩阵也要覆盖手机退化路径

TwinShelf 是大屏平行视界 Demo,但同一应用仍然要在普通手机上运行。

最终 CI 还保留一个非主计分的兼容检查:

Phone
NavigationMode.Stack
article_042

确认平行视界相关 Store 不会要求:

必须有第二栏
必须有 Virtual Workspace
必须能 In-App Split

这些增强能力不可用时,基础阅读仍然成立。

主 30 次矩阵聚焦大屏主线,手机退化作为 Compatibility Gate 单独记录。

三十一、性能指标的 P95 比平均值更容易发现偶发抖动

例如:

Fullscreen
avg=7.1ms
p95=10.8ms

如果下一版:

avg 还是 7.2ms
p95 却变成 28ms

说明平均体验没明显变化,但偶发转场已经变差。

所以 TwinShelf 不只看 avg。

每一个关键操作都保存:

avg
p95
max
sampleCount

封面为了简洁展示 avg / p95,完整 Acceptance Snapshot 还保留 max。

三十二、PASS 由三类 Gate 共同决定

最终不是:

30 次没崩
→ PASS

而是:

FUNCTION_GATE
路由 / 比例 / 全屏 / 容器 / Split 逻辑正确

STATE_GATE
文章 / 选中 / 滚动 / 草稿 / Quote 连续

RESOURCE_GATE
Window / Snapshot / SubNavStack / 临时请求全部收口

性能基线单独产生:

PERF_PASS
PERF_WARN
PERF_FAIL

当前三类功能门和性能门全部满足,所以:

status=PASS

三十三、系列结束后真正值得留在仓库里的是状态协议和回归 Runner

文章完成以后,最有长期价值的是:

RouteSnapshot
FullscreenSnapshot
ReaderWorkspaceSnapshot
InAppSplitSnapshot
RouteConsistencyAssert
WindowStateProbe
SnapshotLeakProbe
TwinShelfRegressionRunner

以后平行视界配置字段、系统版本或产品页面继续变化,只要更新 Fixture 和 Baseline,就可以重新证明:

左栏、右栏、全屏、横屏、虚拟容器和应用内分屏之间,业务状态仍然有清楚边界。

这才是 TwinShelf 六篇真正形成的工程资产。

三十四、最终验收还要检查日志本身是否可追踪

多轮回归结束后,所有关键日志都带 taskId / profile / cycle 三个维度。这样 30 次运行不会混成一串看不懂的 HiLog;任何一次异常都能回到准确场景、准确轮次和准确状态快照。TwinShelf 也不会只保留最终 PASS,而是把每轮的断言结果和关键耗时写进 Acceptance Report。

这一步看起来和 UI 无关,却决定后续问题是否真的能复现。平行视界同时涉及路由、窗口、系统栏、方向和多个内部 Store,没有结构化日志,线上偶发状态错位几乎无法还原。最终系列因此把“能追踪”也视为工程完成度的一部分。

这些报告和状态快照会继续作为下一次系统升级与功能调整时的对照基线,避免后续改动重新回到人工猜测和临时截图验证。

参考资料

  • 2026 年 6 月开发者月刊:HarmonyOS 7 平行视界新增 1:2 / 2:1 等比例:
    https://developer.huawei.com/consumer/cn/monthly/202606
  • HarmonyOS 平行视界官方示例:
    https://developer.huawei.com/consumer/cn/samples/
  • 多设备通用适配指南:
    https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/
  • Navigation 分栏开发:
    https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐