文件批量整理、图片备份和资料同步都可能持续上报进度。当任务执行层每秒产生数十次更新时,球态入口只需要展示一个概要百分比,展开窗口需要展示当前任务与操作按钮,完整任务中心还要保留队列顺序和历史记录。若三个界面都直接监听原始事件,每个进度变动都会触发多份状态写入,页面树更新与布局计算可能集中到同一时间段,反而让原本轻量的交互变得迟缓。

此前七篇已经建立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。

DevEco Studio任务事件与浮层开发环境

四、中间态合并,终态即时送达

把节流器放在仓库与视图之间,而不是放在任务执行入口处。下面用固定的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滑动卡顿分析。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐