这次不是在研究闪控窗怎么开、怎么调大小,而是处理它被用户关掉以后,业务任务要不要跟着停。Demo 名为 FloatTaskRelay,任务 ID 固定为 export_20261001_05。任务在闪控窗隐藏时继续执行,72% 写入检查点,最终完成到 100%,重新打开页面以后恢复为 RESTORED。

一、窗口消失了,不代表任务也应该消失

最开始我把“窗口”和“任务”绑得太紧,用户关闭闪控窗时顺手 cancel 了导出。可用户只是暂时不想看小窗,不代表希望已经跑到 72% 的任务重新开始。

所以这次我把问题重新定义成两套状态:

  • Window:VISIBLE / HIDDEN
  • Worker:IDLE / RUNNING / COMPLETED / FAILED
  • 业务状态:RUNNING / CHECKPOINTED / RESTORED
  • Progress:独立保存,不由窗口是否可见决定

固定数据:

  • Task ID:export_20261001_05
  • 窗口关闭时 Window:HIDDEN
  • Worker:RUNNING
  • Checkpoint:72%
  • 最终 Progress:100%
  • Restore Count:1
  • Result File:clip_20261001_05.mp4

二、先把耗时工作从页面方法里搬出去

这个 Demo 的导出任务控制在几分钟以内,所以我用 TaskPool 演示短时并发任务;如果任务会长期常驻、持续超过数分钟,就不能把 TaskPool 当“无限后台线程”使用,应该评估 Worker 或系统提供的后台任务机制。

第一段代码解决的是“窗口回调里不能直接做重活”。

import { taskpool } from '@kit.ArkTS'

export interface ExportPayload {
  taskId: string
  source: string
  output: string
}

@Concurrent
export function exportWorker(payload: ExportPayload): string {
  for (let progress = 1; progress <= 100; progress++) {
    // Demo 中用计算和分段写入模拟导出过程
    if (progress % 4 === 0) {
      taskpool.Task.sendData(progress)
    }
  }
  return payload.output
}

任务函数做成独立 @Concurrent 函数,参数只带可传递的数据,不把页面对象、窗口对象、UI 状态直接塞给子线程。这样页面消失以后,任务不需要继续持有组件实例。

三、进度不是一个 Text 数字,而是一份检查点

最开始我只用 @State progress 更新 UI。窗口还在时没问题,一旦页面重建,72% 就变回 0。

所以我给任务加了一层 TaskCheckpointStore。它解决的是“UI 不在场时,进度还要有地方落”。

import { preferences } from '@kit.ArkData'

export class TaskCheckpointStore {
  constructor(private store: preferences.Preferences) {}

  async save(taskId: string, progress: number): Promise<void> {
    await this.store.put(`${taskId}:progress`, progress)
    await this.store.flush()
  }

  async read(taskId: string): Promise<number> {
    return await this.store.get(
      `${taskId}:progress`,
      0
    ) as number
  }
}

我没有每 1% 都写一次磁盘,而是让 Demo 每隔一个小批次更新检查点。本轮关闭闪控窗时刚好记录到 72%。

四、窗口关闭只改 Window,不去碰 Worker

闪控窗关闭回调只更新 Window=HIDDEN,任务继续由 ExportTaskService 管,不能顺手 cancel。

private onFloatWindowHidden(): void {
  this.windowState = 'HIDDEN'
  hilog.info(0x0000, 'FloatTask',
    `Window: VISIBLE -> HIDDEN, task=${this.taskId}`)
}

private async startExport(): Promise<void> {
  const payload: ExportPayload = {
    taskId: 'export_20261001_05',
    source: this.sourceUri,
    output: 'clip_20261001_05.mp4'
  }

  const task = new taskpool.Task(exportWorker, payload)

  task.onReceiveData(async (progress: object) => {
    this.progress = progress as number
    await this.checkpointStore.save(this.taskId, this.progress)
  })

  this.workerState = 'RUNNING'
  this.resultFile = await taskpool.execute(task) as string
  this.progress = 100
  this.workerState = 'COMPLETED'
}

DevEco 图里正好能看到这一阶段:

start export task, id=export_20261001_05
Window: VISIBLE -> HIDDEN
Worker remains RUNNING
progress=72 checkpoint=72
State: RUNNING -> CHECKPOINTED

五、重新打开闪控窗时,不重新创建第二个任务

窗口重新打开后,页面重建很容易误建第二个 TaskPool Task,导致同一文件导出两遍。

恢复时先查旧任务状态,再决定 UI 怎么显示。Demo 里由运行时服务持有任务引用。

async restoreViewState(): Promise<void> {
  const checkpoint = await this.checkpointStore.read(
    'export_20261001_05'
  )

  this.checkpoint = checkpoint
  this.restoreCount += 1

  const runtime = this.taskService.getRuntime(
    'export_20261001_05'
  )

  if (runtime?.state === 'COMPLETED') {
    this.progress = 100
    this.workerState = 'COMPLETED'
    this.resultFile = 'clip_20261001_05.mp4'
    this.state = 'RESTORED'
    return
  }

  this.progress = checkpoint
  this.workerState = runtime?.state ?? 'IDLE'
}

六、最终验收看的是:窗口回来以后,结果能不能对上

最终运行结果:

  • Task ID:export_20261001_05
  • Window:VISIBLE
  • Worker:COMPLETED
  • Progress:100%
  • Checkpoint:72%
  • Restore Count:1
  • Result File:clip_20261001_05.mp4
  • State:RESTORED

VISIBLE -> HIDDEN -> RUNNING -> CHECKPOINTED -> COMPLETED -> RESTORED

七、这个方案的边界,比代码本身更重要

有三个边界必须写清楚。

第一,TaskPool 不是“应用进程被杀以后还能继续跑”的承诺。本 Demo 只讨论闪控窗隐藏、页面重建而进程仍存活的场景;跨进程、长时间后台执行要使用对应后台能力。

第二,Preferences 只保存轻量检查点,不保存视频分片和大对象。真正断点续作还要记录输入版本、临时文件和校验信息。

第三,检查点存在不等于任务还活着,恢复页面必须同时查询运行时任务状态。

八、我最后保留的工程规则

这次 Demo 写完以后,我把规则缩成四句:

窗口显隐归窗口,任务运行归任务。

检查点是恢复依据,不是最终结果。

页面重建先查旧任务,再决定要不要创建新任务。

短时并发可以用 TaskPool,长时常驻任务要重新评估并发模型和后台策略。

FloatTaskRelay 从 72% 隐藏窗口开始,任务继续跑到 100%,重新打开后只恢复一次,输出 clip_20261001_05.mp4。这次解决的不是“闪控窗关掉以后怎么继续显示进度”,而是把窗口生命周期和业务任务生命周期拆成两条可以独立验证的线。

Logo

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

更多推荐