HarmonyOS 7 平行视界:交互性能诊断与发布复核(连载06-06)【共创稿事节】
双栏页面进入收尾阶段时,验收表上最常见的一句话是“功能都能点”。这句话只能说明路径可达,离长期可用还差一段距离。用户用平板快速滑动几十条消息,选中一条后立即返回列表,又将窗口收窄到单栏;如果列表每次都整批重建,详情切换夹杂同步解析,界面便会在高频操作中暴露出性能问题。比单次打开快不快更值得关注的,是同一条操作链上每一环能否保持连贯。
前五篇已经确定双栏导航、返回路径、共享选中态、可拖动宽度和阅读状态恢复的职责。本篇把这些机制放到同一张发布检查清单中:以真实交互过程为单位识别耗时,将高频列表更新与详情加载隔离,再检查窗口切换、回退和数据恢复是否破坏用户的阅读位置。

把性能问题还原成可追踪的操作链
“滑动不顺”不是一个适合直接修复的技术问题。需要把用户操作拆成连续事件:列表开始滑动、停住、点开详情、详情开始获取数据、返回、再次选中、调整分栏宽度。每个事件都对应明确的触发者,才有机会判断瓶颈在状态刷新、组件布局、图片解码,还是业务处理。
在消息阅读工作台中,列表项只展示标题、摘要、时间和已读标记;正文和附件属于详情区。滚动列表不应驱动详情正文反复装载,详情刷新也不应让所有列表项重新构建。页面可以让选中 ID 保持在统一状态源中,同时将完整正文、附件和富文本解析放在异步业务层。共享的是必要标识,不是全部重量级对象。
| 用户操作 | 需要关注的变化 | 不应连带发生的行为 |
|---|---|---|
| 快速滑动列表 | 可见列表项位置与少量预取项 | 批量构建所有正文组件 |
| 点开另一条消息 | 选中 ID、详情请求及高亮 | 刷新不相关列表行的数据 |
| 返回列表 | 详情导航栈与当前高亮 | 清空已加载的消息索引 |
| 调整分栏宽度 | 左右栏尺寸及文本排版 | 重置消息选中项和滚动锚点 |
| 前后台切换 | 会话快照与必要资源 | 重复保存整棵组件结构 |
用懒加载约束列表的节点数量
ArkUI List 搭配 LazyForEach,可以按照可见区域构建列表项;而 List 搭配普通 ForEach 时会一次性创建所有子组件。对于条目持续增加的阅读工作台,懒加载应成为列表结构设计的一部分。LazyForEach 依赖 IDataSource 提供数据和变更通知,删除、替换与插入时必须保持监听器和数据一致,不能只修改普通数组而忽略 UI 更新。
// message/MessageSource.ets:一个可独立复用的列表数据源
export interface MessageRow {
id: string;
title: string;
summary: string;
read: boolean;
}
export class MessageSource implements IDataSource {
private rows: MessageRow[];
private listeners: DataChangeListener[] = [];
constructor(rows: MessageRow[]) { this.rows = rows; }
totalCount(): number { return this.rows.length; }
getData(index: number): MessageRow { return this.rows[index]; }
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) this.listeners.push(listener);
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index: number = this.listeners.indexOf(listener);
if (index >= 0) this.listeners.splice(index, 1);
}
replace(index: number, row: MessageRow): void {
if (index < 0 || index >= this.rows.length) return;
this.rows[index] = row;
this.listeners.forEach((listener: DataChangeListener) => listener.onDataChange(index));
}
}
这段实现刻意只提供单行替换。一次只改已读标记时,知道具体索引比直接宣布全量重载更有利于控制刷新范围。真实业务还应为插入、删除、排序设计相应的通知,并将来自网络的消息按 ID 合并;排序导致索引变化时,要按最终数据顺序通知,避免数据位置与高亮错位。
列表项的 key 也必须稳定。若使用数组索引充当标识,插入一条置顶消息后,旧索引代表的内容会改变,容易引出子组件状态错配。选择消息自身不会变化的业务 ID,更符合消息工作台的更新特征。
// pages/InboxPane.ets:列表部分,正文独立交给详情区
import { MessageSource, MessageRow } from '../message/MessageSource';
@Component
export struct InboxPane {
private source: MessageSource = new MessageSource([
{ id: 'a01', title: '版本评审', summary: '评审意见已汇总', read: false },
{ id: 'a02', title: '交付清单', summary: '请复核待办项', read: true }
]);
@Prop selectedId: string = '';
onSelect: (id: string) => void = () => {};
build() {
List({ space: 4 }) {
LazyForEach(this.source, (row: MessageRow) => {
ListItem() {
Column({ space: 6 }) {
Text(row.title).fontSize(17).fontWeight(FontWeight.Medium)
Text(row.summary).fontSize(13).fontColor('#667085')
}
.padding(14).width('100%')
.backgroundColor(this.selectedId === row.id ? '#E8F2FF' : '#FFFFFF')
}.onClick(() => this.onSelect(row.id))
}, (row: MessageRow) => row.id)
}
.cachedCount(3)
.width('100%')
.height('100%')
}
}
这里的 cachedCount(3) 是预取范围的初始配置值,不是对所有设备都成立的最佳数值。缓存数提高通常会增加预创建成本和内存使用,实际取舍应根据行高、图像比例和滚动特征确定。代码示例还需要在使用处从数据源模块导入 MessageSource 和 MessageRow,并由父级维护 selectedId,让导航切换不改变消息身份。

不要在渲染路径里做同步重活
如果详情包含 Markdown 解析、附件信息整理、复杂排序或较大的 JSON 数据转换,就需要把它们与组件 build() 分离。build() 应根据已有状态描述界面,而不是在构建期间继续修改状态并触发新的重建。数据请求也不应由每一次状态刷新重复启动;把获取动作放到明确的用户行为或页面生命周期阶段,再用请求序号排除过期响应,能同时减少重复任务与闪烁。
诊断时,建议围绕一段固定的用户路径采样:连续滑动、点击列表项、切换详情、返回并切换窗口宽度。DevEco Profiler 能从 Frame 泳道观察卡顿区间,再结合业务操作和 ArkUI 调用栈寻找可疑工作。不能仅凭某一次主观感受判断已经优化成功;更有价值的是保存相同输入条件下的前后比较,包括页面结构、数据量、图片资源和设备刷新率。
图片资源往往容易被忽略:列表缩略图不需要下载或解码成详情大图的尺寸。行组件避免不必要的阴影、模糊和深层容器嵌套;当更新只涉及已读标志时,尽量将局部可变数据落在局部组件中,而不要把整份富文本对象塞进高频列表项。系统级导航和业务级数据仓库各自负责自己的部分,性能问题才不会互相放大。
把发布前复核写成可执行的检查场景
相同代码在不同窗口尺寸下,会出现不同的呈现路径。团队可以把检查对象划分为三个矩阵,而不是用一台设备的一次体验代替整体结论。
| 复核维度 | 操作组合 | 重点观察 |
|---|---|---|
| 布局 | 宽屏双栏、窄窗单栏、分隔线拖拽 | 是否有裁剪、遮挡和不可点击区域 |
| 导航 | 列表→详情→深层内容→返回 | 路由栈、选中高亮和焦点是否一致 |
| 数据 | 刷新列表、收到新增项、切换已读 | 数据 ID 和显示位置是否对应 |
| 生命周期 | 后台返回、窗口重建、阅读恢复 | 选中项及文章锚点是否复原 |
| 性能 | 长列表滚动、重复打开详情、缩放窗口 | 帧耗时、内存变化及重复任务 |
| 无障碍 | 放大字体、键盘焦点、屏幕阅读 | 信息顺序与可操作控件是否完整 |
实际组织复核时,特别要把“在加载详情期间切换另一个消息”和“拖动分栏时恰好有列表更新”列入场景。这两条路径能同时检验异步数据序号、选中态、节点复用和窗口适配。如果业务逻辑只是把结果直接写进当前组件,过期请求容易覆盖新详情;如果业务层使用 ID 匹配和请求序号,界面更容易保持可解释的状态。
可以用一段轻量的纯函数为发布检查建立确定的判定基础,例如把窗口可用宽度、左栏最小宽度和右栏最小宽度分离计算。它并不替代 Navigation 自身的测量布局,而是让工程侧对于最小显示条件拥有清晰的输入输出。
export interface PaneDecision {
useSplit: boolean;
leftMin: number;
rightMin: number;
}
export function decidePane(widthVp: number): PaneDecision {
const leftMin: number = 260;
const rightMin: number = 360;
const divider: number = 1;
return {
useSplit: widthVp >= leftMin + rightMin + divider,
leftMin: leftMin,
rightMin: rightMin
};
}
// 发布复核用例数据:可用于单元测试进行边界断言
const cases: number[] = [600, 620, 621, 720, 960];
cases.forEach((w: number) => {
const decision: PaneDecision = decidePane(w);
console.info(`width=${w}, split=${decision.useSplit}`);
});
边界值 621vp 来自本例的业务约束,不是平台统一规定的平行视界分栏阈值。一个工程不应把“平板”与“必然双栏”等同起来,真正影响布局的是应用当时可用的内容区域。用户把平板窗口缩窄时,单栏的操作和导航也应保持完整。

收官检查应留下一份可信的工程依据
这一套连载建立的工程边界是明确的:Navigation 管页面容器与回退;业务仓库管消息身份、阅读状态及版本合并;布局策略根据窗口空间决定单栏或双栏;持久化保存业务上有意义的锚点;列表使用稳定键和按需构建控制更新范围。发布前复核把这些机制连起来,目的不是追求某个好看的性能数字,而是避免一个地方的优化破坏其他地方的连续性。
最终验收可将操作步骤、输入消息集合、窗口宽度和发现的问题对应起来,由开发团队在可复现条件下逐项确认。对于尚未达到预期的场景,保留追踪点并明确修复位置;对于已满足要求的场景,仍需在依赖升级或布局改动之后重跑相关路径。大屏体验真正经得起使用的标志,是用户不需要意识到页面栈、数据源、缓存和窗口生命周期正在协作。
参考文档
更多推荐

所有评论(0)