一个资料整理任务已处理到68%,用户切换到其他应用。回到任务中心后,球态还停留在68%,展开窗口显示“处理中”,实际上执行服务已经被中断;再按一次继续,又出现两次启动请求。问题不是进度控件显示得慢,而是前后台切换时执行状态、持久快照和界面订阅没有按统一规则衔接。

此前五篇已经建立了任务状态机、窄宽布局、球窗切换命令、进度事件版本与跨页面共享快照。这些设计覆盖同一进程内的界面变化,但前后台切换带来了新的时序:UI可能暂时不可交互,定时器可能停止,进程也可能重建。恢复时必须先确认任务执行事实,再恢复界面上的操作入口。本文从UIAbility生命周期出发,给出一个可以嵌入任务中心的恢复链路。

前后台生命周期与任务恢复封面

一、将三种状态分开管理

先区分三类不同的事实。TaskPhase表示任务业务阶段;SurfaceMode表示球态或展开窗口的当前外观;AppVisibility表示UI当前是否处在前台。它们有联系,却不能互相代替。例如切到后台不一定代表任务暂停,而前台也不意味着任务一定继续执行。

类型代表字段管理者恢复策略
执行事实phase、completed、revision任务执行仓库重新查询并核对执行结果
展示意图球态、窗态、当前任务表面状态控制器根据可用窗口重新渲染
生命周期前台、后台、页面订阅UIAbility与组件暂停UI刷新、重建订阅
可恢复条件资源标识、断点位置任务适配器检查资源仍可访问后再尝试恢复

尤其不要把“后台”直接写成PAUSED:后台只是应用生命周期变化,任务是否继续取决于它的执行方式及系统允许的行为。任务仓库发布业务状态,UIAbility负责触发刷新和持久化操作,轻量窗口只订阅当前投影。

二、为恢复设计明确的中间阶段

任务从内存恢复时,先显示一个独立的RECONCILING阶段,避免把旧快照中的RUNNING直接当作正在运行。后台存储记录表明的是最后一次已知进度,并不能替代重新核对执行服务。若任务有可靠的断点信息,在确认数据源可用后才能继续;否则界面应给出重试或重新开始的选项。

export enum TaskPhase {
  QUEUED = 'queued',
  RUNNING = 'running',
  PAUSED = 'paused',
  RECONCILING = 'reconciling',
  DONE = 'done',
  FAILED = 'failed'
}

export class TaskSnapshot {
  constructor(
    public id: string,
    public label: string,
    public phase: TaskPhase,
    public completed: number,
    public total: number,
    public revision: number,
    public checkpoint: string = ''
  ) {}
}

export function toRecoveryState(saved: TaskSnapshot): TaskSnapshot {
  const phase: TaskPhase = saved.phase === TaskPhase.RUNNING
    ? TaskPhase.RECONCILING : saved.phase
  return new TaskSnapshot(saved.id, saved.label, phase,
    Math.max(0, saved.completed), Math.max(0, saved.total),
    saved.revision, saved.checkpoint)
}

revision保留给执行事件的顺序控制。恢复流程自身应有单独的epoch,因为页面可能先后触发多个刷新请求。这两个序列不能混用:revision解释同一任务哪条进度事件更新,epoch解释哪次恢复查询有资格更新当前UI。

三、持久化只保存能够重建的业务快照

Preferences适合保存少量任务进度和展示选择,例如当前关注任务ID、任务阶段和断点令牌。它不适合把庞大的文件列表、密集进度事件和全部业务日志塞进一个键。对于完整任务队列,应逐步迁移到适当的数据库结构;这里先展示单个关注任务的可恢复快照。

import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'

export class TaskCheckpointStore {
  private store: preferences.Preferences

  private constructor(store: preferences.Preferences) {
    this.store = store
  }

  static async open(ctx: common.UIAbilityContext): Promise<TaskCheckpointStore> {
    const options: preferences.Options = { name: 'task_checkpoint' }
    const value = await preferences.getPreferences(ctx, options)
    return new TaskCheckpointStore(value)
  }

  async save(item: TaskSnapshot): Promise<void> {
    await this.store.put('focused_task', JSON.stringify(item))
    await this.store.flush()
  }

  async read(): Promise<TaskSnapshot | undefined> {
    const value = await this.store.get('focused_task', '')
    if (typeof value !== 'string' || value.length === 0) { return undefined }
    try {
      const data = JSON.parse(value) as TaskSnapshot
      if (typeof data.id !== 'string' || typeof data.revision !== 'number') {
        return undefined
      }
      if (typeof data.completed !== 'number' || typeof data.total !== 'number') {
        return undefined
      }
      return toRecoveryState(data)
    } catch (_) {
      return undefined
    }
  }
}

持久化需要有一个单一写入者。任务服务每当经过“已排队、暂停完成、执行完成、失败确认”等关键节点,便向存储层提交新快照。高频进度可合并,避免多个异步flush交错。上述类展示最基本的读写;在正式集成时,应再用一条Promise提交链串行化save,沿用本系列第五篇的事件版本处理,不让旧版本回写。

开发环境中的生命周期与任务仓库

四、用UIAbility连接前台恢复与后台快照

UIAbility.onForeground适合发起数据重新核对、恢复订阅和刷新快照;onBackground适合停止仅服务于界面的刷新任务,提交近期业务状态。这里不要把长时间IO塞入生命周期回调,也不要认为回调后的异步操作一定可以无限期执行。

import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { TaskRuntime } from '../common/TaskRuntime'

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    TaskRuntime.getInstance().configure(this.context)
  }

  onWindowStageCreate(stage: window.WindowStage): void {
    stage.loadContent('pages/Index')
  }

  onForeground(): void {
    TaskRuntime.getInstance().enterForeground()
  }

  onBackground(): void {
    TaskRuntime.getInstance().enterBackground()
  }
}

TaskRuntime是应用层的类,不是系统接口。它的enterBackground只停掉轮询与视觉刷新,并发起一次轻量状态保存;enterForeground会从后台读回断点并核对当前执行状态。若应用根本没有可靠的后台执行渠道,应在界面显示“待恢复”,等待用户确认重新开始。不同设备和不同任务类型的执行条件要通过实际产品采用的任务机制处理,UI层不要擅自推断。

五、恢复必须防止旧请求覆盖新状态

例如用户快速在两个应用间切换,前台恢复A尚未结束,后台切换又开始,第二次回到前台时恢复B发起。若A最后才返回,就会用旧状态覆盖B。采用递增epoch可以在异步回调里识别过期结果。

export interface ExecutionProbe {
  query(taskId: string): Promise<TaskSnapshot | undefined>
}

export class RecoveryCoordinator {
  private epoch: number = 0
  private active: boolean = false
  constructor(private probe: ExecutionProbe) {}

  async resume(saved: TaskSnapshot,
    publish: (item: TaskSnapshot) => void): Promise<void> {
    this.active = true
    const ticket: number = ++this.epoch
    publish(toRecoveryState(saved))
    try {
      const live = await this.probe.query(saved.id)
      if (!this.active || ticket !== this.epoch) { return }
      if (live && live.revision >= saved.revision) {
        publish(live)
      } else {
        publish(toRecoveryState(saved))
      }
    } catch (_) {
      if (this.active && ticket === this.epoch) {
        publish(toRecoveryState(saved))
      }
    }
  }

  background(): void {
    this.active = false
    this.epoch++
  }
}

这里的query由真实任务执行适配器实现,返回执行服务确认的任务快照。若查询失败,保留待恢复状态而不是默认为运行中。执行指令还需要单独的幂等键:例如taskId + operation + expectedRevision,多次点击继续时只允许一个有效请求进入执行仓库。页面中的“提交中”只是交互反馈,阶段改变必须等待确认事件。

六、球态与展开窗口怎样恢复同一份状态

轻量任务球只展示进度环与简短状态,展开窗口展示详细信息和按钮,任务中心则展示完整队列。页面重新出现时,各视图都应从仓库当前快照派生投影,不能各自从Preferences读取后单独维护。采用AppStorage或应用级可观察仓库传递最新快照,存储中可以先使用字符串格式,避免V1观察深层对象变化带来的不确定刷新。

export class TaskProjectionPublisher {
  publish(item: TaskSnapshot): void {
    AppStorage.setOrCreate<string>('task.focus.snapshot', JSON.stringify(item))
  }
}

@Component
struct TaskBallSummary {
  @StorageLink('task.focus.snapshot') snapshotText: string = ''

  build() {
    Column({ space: 4 }) {
      Text('轻任务')
      Text(this.snapshotText.length > 0 ? '查看当前进度' : '暂无任务')
    }
  }
}

紧凑球态的交互只负责发出“展开”意图,展开窗口重新读取同一个task.focus.snapshot。需要显示百分比时由统一的任务投影函数转换,避免球态与窗口对边界值采用不同的取整方式。组件销毁后应撤销定时器和服务订阅;重新创建后以最新快照初始化,而不是回放全部历史进度事件。

轻任务恢复后的手机界面

七、按故障现象检查生命周期链路

回到前台时进度倒退。 检查保存的revision是否高于实时查询结果,以及后台是否有旧提交。恢复查询应以版本和执行阶段综合判断,不能盲信某个页面缓存。

任务显示运行中但无进度变化。 先看执行适配器是否确认了工作仍在进行,再看UI轮询是否恢复。确认执行中后可以恢复刷新;执行端已中止时,应给用户提供继续或重新开始的明确入口。

从球态展开窗口后出现两个订阅者。 检查页面销毁与新建时对订阅、定时器是否对称处理。业务仓库只保留一个状态发布源,多个视图各自按生命周期订阅,不重复启动任务。

后台切换时界面卡顿。 不应在onBackground同步处理大量文件数据。提前按任务节点保存小快照,生命周期回调仅安排轻量收尾。对耗时操作,采用已有的异步任务服务并控制资源使用。

八、结语

前后台恢复的重点不是“返回前台就重新把任务标记成运行中”,而是把最后一次已知快照、执行端查询结果和当前页面订阅重新对齐。使用RECONCILING阶段承接不确定的执行事实,用递增epoch过滤过期恢复,用单一仓库发布视图投影,球态、展开窗口与任务中心才能在不同生命周期下表现一致。

相关接口可参考华为开发者文档:UIAbility与窗口生命周期、Preferences用户首选项、ArkUI AppStorage状态关联、生命周期耗时操作排查。

Logo

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

更多推荐