HarmonyOS 7闪控窗与闪控球连载08(共08篇):高频任务刷新与性能优化【共创稿事节】
文件批量整理、图片备份和资料同步都可能持续上报进度。当任务执行层每秒产生数十次更新时,球态入口只需要展示一个概要百分比,展开窗口需要展示当前任务与操作按钮,完整任务中心还要保留队列顺序和历史记录。若三个界面都直接监听原始事件,每个进度变动都会触发多份状态写入,页面树更新与布局计算可能集中到同一时间段,反而让原本轻量的交互变得迟缓。
此前七篇已经建立TaskPhase状态机、球窗共享快照、幂等切换、权限引导与前后台恢复机制。本篇作为收束,重点讨论事件处理频率和界面呈现频率分离:业务事件完整进入状态仓库,UI只接收必要的可见变化;完成、失败、取消这类终态仍立即送达。这样才能在保证任务事实正确的前提下减少无意义的重绘。

一、先找到三种刷新源
轻任务系统的刷新并非只有百分比。任务执行服务会产生字节计数和阶段转换;用户点击球态或展开窗口会修改外观;前后台切换和窗口重建会重新挂载订阅。若事件处理器直接写入AppStorage或组件的多个状态变量,某个单一进度事件可能引起任务列表、球态、展开卡片和统计区域同时更新。
应该先辨认三条链路。执行链路完整接收并归档任务事件,承担顺序与错误校验;投影链路把业务快照转换为用户看得懂的状态;呈现链路只在展示内容发生实质变化时通知对应组件。一个已上传61.04%的任务连续上报61.08%、61.11%时,球态展示的整数依然是61%,没必要重新生成浮层内容。不过这些细微事件仍应由业务层维护,避免丢失精确进度。
| 链路 | 保留数据 | 处理原则 |
|---|---|---|
| 任务执行与仓库 | 精确字节量、阶段、版本 | 按版本接受合法事件 |
| 球态摘要 | 当前任务、整数百分比、终态 | 相同可见内容不重复通知 |
| 展开窗口 | 当前任务及操作按钮 | 合并连续中间态,终态优先 |
| 完整任务中心 | 队列、错误提示、历史 | 需要时读取最新仓库快照 |
二、先保证事件归并的正确性
合并刷新之前,先要建立数据版本约束。不能用时间戳替代业务版本,因为不同来源的时间并不总是有全局顺序;也不能简单选择百分比较大的事件,因为失败后的重试可能开启新的执行阶段。延续前文的revision字段,同一个任务只有更高版本才能覆盖现有快照。阶段改变和进度改变都由任务执行层明确发布。
export enum TaskPhase {
WAITING = 'waiting', RUNNING = 'running',
PAUSED = 'paused', DONE = 'done', FAILED = 'failed'
}
export class TaskSnapshot {
constructor(public id: string,
public revision: number,
public phase: TaskPhase,
public progress: number,
public title: string) {}
}
export class TaskSnapshotStore {
private records: Map<string, TaskSnapshot> = new Map()
apply(incoming: TaskSnapshot): boolean {
const old: TaskSnapshot | undefined = this.records.get(incoming.id)
if (old && incoming.revision <= old.revision) return false
if (!Number.isFinite(incoming.progress)) return false
const percent = Math.min(100, Math.max(0, incoming.progress))
this.records.set(incoming.id, new TaskSnapshot(incoming.id,
incoming.revision, incoming.phase, percent, incoming.title))
return true
}
get(id: string): TaskSnapshot | undefined {
return this.records.get(id)
}
}
这段代码负责顺序与数值规范化,而不是负责屏幕刷新。状态仓库接受事件后,可以触发持久化或其他业务逻辑;UI投影是否刷新由下一层决定。若用户把任务取消,不能依靠降低刷新频率来延迟取消的呈现,否则球态会继续显示“处理中”,产生误解。
三、以可见字段生成投影
球态通常只展示一个主任务的整数百分比以及阶段图标;展开窗口则还要展示标题与按钮状态。把整个业务对象绑定到每个视图会扩大观察范围。可以建立独立的轻量投影,只有可见字段真正变化时才产生新对象。
export class BallViewState {
constructor(public id: string, public percent: number,
public phase: TaskPhase, public label: string) {}
}
export function projectBall(task: TaskSnapshot): BallViewState {
return new BallViewState(task.id, Math.floor(task.progress),
task.phase, task.title)
}
export function sameBall(a: BallViewState | null,
b: BallViewState): boolean {
return a !== null && a.id === b.id &&
a.percent === b.percent && a.phase === b.phase && a.label === b.label
}
Math.floor只用于视觉整数展示,仓库里的精确进度保持不变。任务从RUNNING进入DONE时,哪怕整数百分比没有变化也要刷新,因为按钮、颜色与说明已经发生了变化。用户切换当前观察任务时也一样,即便两个任务都是65%,任务ID变更仍需通知UI。

四、中间态合并,终态即时送达
把节流器放在仓库与视图之间,而不是放在任务执行入口处。下面用固定的100毫秒窗口举例:窗口内的中间事件仅保留最后一份可见快照;终态直接推送并取消待发送计时器。窗口长度是便于理解的初始值,实际应根据交互目标、设备状况与性能分析结果调节。
export class VisibleUpdateGate {
private timer: number | undefined = undefined
private pending: BallViewState | null = null
private delivered: BallViewState | null = null
private active: boolean = true
constructor(private publish: (state: BallViewState) => void,
private intervalMs: number = 100) {}
accept(state: BallViewState): void {
if (!this.active) return
if (sameBall(this.delivered, state)) return
const terminal: boolean = state.phase === TaskPhase.DONE ||
state.phase === TaskPhase.FAILED || state.phase === TaskPhase.PAUSED
this.pending = state
if (terminal) {
this.flush()
return
}
if (this.timer !== undefined) return
this.timer = setTimeout((): void => this.flush(), this.intervalMs)
}
private flush(): void {
if (this.timer !== undefined) clearTimeout(this.timer)
this.timer = undefined
const next: BallViewState | null = this.pending
this.pending = null
if (next && !sameBall(this.delivered, next)) {
this.delivered = next
this.publish(next)
}
}
dispose(): void {
this.active = false
if (this.timer !== undefined) clearTimeout(this.timer)
this.timer = undefined
this.pending = null
}
}
这里还有两个关键细节。第一,节流器不能跨组件销毁继续推送,因此aboutToDisappear或宿主退出时必须释放订阅与计时器。第二,终态不仅是100%,失败和暂停同样要求立即呈现;若任务进入新一轮重试,仓库通过新的revision接纳事件,投影才转回运行态。刷新策略不会替业务层决定是否允许重试。
五、浮层内容应更新,而不是反复创建
在应用当前窗口内使用OverlayManager创建轻量入口时,应把节点创建与内容更新分离。系统提供ComponentContent.update用来更新Builder参数,OverlayManager则负责节点的添加、隐藏和移除。每次百分比变化都重新创建节点,会产生额外对象与布局开销,也容易留下未正确释放的旧浮层。
import { ComponentContent } from '@kit.ArkUI'
class BubbleParams {
constructor(public title: string, public percent: number) {}
}
@Builder function BubbleView(args: BubbleParams) {
Column({ space: 4 }) {
Text(`${args.percent}%`).fontSize(22)
.fontWeight(FontWeight.Bold).fontColor(Color.White)
Text(args.title).fontSize(11).fontColor(Color.White).maxLines(1)
}
.width(88).height(88)
.justifyContent(FlexAlign.Center)
.backgroundColor('#166DE3').borderRadius(44)
}
export class BubblePresenter {
private content: ComponentContent<BubbleParams>
constructor(context: UIContext, initial: BubbleParams) {
this.content = new ComponentContent(context,
wrapBuilder<[BubbleParams]>(BubbleView), initial)
}
getContent(): ComponentContent<BubbleParams> { return this.content }
update(state: BallViewState): void {
this.content.update(new BubbleParams(state.label, state.percent))
}
dispose(): void { this.content.dispose() }
}
在组件中创建BubblePresenter后,先将其内容交给getUIContext().getOverlayManager()管理,再通过VisibleUpdateGate发布有效变化。隐藏浮层时可以保留业务仓库;移除浮层时应完成移除、订阅注销与dispose的配套处理。ComponentContent更新方法面向它自身的Builder参数,不能把它当成跨进程的任务数据总线。若产品需要跨应用显示的系统级入口,应以对应公开能力与场景约束为准,不能把应用窗口内浮层与系统窗口划等号。
六、不要让任务列表成为新的渲染热点
完整任务中心的卡片比球态复杂,包含文件名、进度条、阶段提示和操作按钮。对于任务数量较多的列表,建议采用有稳定键的懒加载数据源,把排序和状态归并放在数据层完成,再一次性发布必要的界面变更。对单个任务的中间进度,只需更新该任务可见字段,不应重新构造整个历史数组;对于列表分组变化,则由数据源发出相应重排通知。
很多高频刷新问题还源于订阅重复注册。页面进入前台、打开浮层和重新进入任务中心时,如果每一步都新增一个回调而没有匹配解除,同一业务事件会以倍数触发UI更新。建议给订阅设置持有者与唯一标识:一个页面实例只注册一次,隐藏页面停止可见更新,销毁时注销;重新显示时从仓库读取最新快照,而不是追赶已经过期的中间事件。
对于展开窗口,按钮状态和任务执行事实也要保持隔离。优化刷新不应该吞掉“暂停请求已接受”“资源选择等待中”等明确的用户反馈。即便进度仍停留在42%,阶段文本与操作按钮发生变化也应立即更新。
七、以固定事件序列完成性能与正确性回归
可以使用一组可控事件作为测试数据:同一任务按版本依次上报60.1、60.2、60.9、61.0、61.1,随后进入暂停;再提供一个旧版本的60.5事件以及一次新版本恢复事件。期望仓库拒绝旧版本,球态可以合并部分整数百分比不变的中间帧,但必须立即展示暂停和恢复后的阶段。另一个序列覆盖浮层关闭、页面销毁、页面重新出现,检查是否存在重复订阅和已经失效的UI回调。
export class TaskRegression {
static check(store: TaskSnapshotStore): string[] {
const issues: string[] = []
store.apply(new TaskSnapshot('transfer', 1,
TaskPhase.RUNNING, 60.1, '文件整理'))
store.apply(new TaskSnapshot('transfer', 3,
TaskPhase.PAUSED, 61.1, '文件整理'))
const accepted: boolean = store.apply(new TaskSnapshot('transfer', 2,
TaskPhase.RUNNING, 60.5, '文件整理'))
const finalState = store.get('transfer')
if (accepted) issues.push('旧版本被接受')
if (!finalState || finalState.phase !== TaskPhase.PAUSED) {
issues.push('阶段状态被覆盖')
}
return issues
}
}
性能侧建议分别记录任务事件输入速率、状态仓库更新次数、球态投影提交次数和实际界面帧表现。尤其注意,减少publish次数不等同于已经获得某个具体帧率提升。应该在相同任务数据、相同窗口尺寸和相同可见组件的条件下采集对比数据,再决定是否进一步调整节流窗口。可视化的中间数据可以放在开发阶段的分析表中,正式产品只保留合理的异常追踪字段。

八、回到轻量协同的工程本质
从第一篇任务队列模型,到球窗布局、幂等切换、触控反馈、跨页面同步、前后台恢复、权限降级,再到这一篇的刷新治理,整套实现始终围绕一个判断:界面是任务状态的投影,不是任务执行事实本身。执行仓库保证版本顺序,投影层判断展示内容是否改变,呈现层只负责当前可见区域。这种分层既保留了用户需要的及时反馈,也让高频事件不至于扩大成全局重复刷新。
后续增加下载、备份或批处理任务时,可以复用这套版本约束和订阅生命周期治理;每引入新的浮层样式,就补充其终态、权限变化与销毁重建的回归序列。轻量交互的价值不在于窗口始终悬浮,而在于任何入口变化都不会破坏任务的连续性和可理解性。
相关开发文档:OverlayManager浮层管理、ComponentContent与update、组件overlay属性、ArkUI滑动卡顿分析。
更多推荐

所有评论(0)