HarmonyOS 7闪控窗与闪控球连载07(共08篇):异常降级与权限处理【共创稿事节】
轻任务中心的执行状态、球态摘要和展开窗口已经通过同一套快照同步,并能在前后台切换时重新核对任务事实。真正上线前还有一条关键路径:当轻量窗口创建失败、当前页面上下文失效、用户没有授权相册资源,或者后台恢复的任务数据已经过期,任务应该怎样继续?如果一个悬浮入口失效就把整个任务标记成失败,用户可能会误以为正在整理的文件也丢失了;如果权限不足时仍自动重试,界面又可能陷入反复弹出请求的循环。
这篇关注能力可用性与任务执行事实的分离。任务能否继续、界面能否悬浮、资源是否已经得到授权,是三个不同判断。闪控球和展开窗口承担快捷入口,任务中心承担稳定的操作入口;某个入口异常时,优先让用户在应用内恢复任务控制,而不是让执行服务跟随界面消失。

一、建立明确的故障分类
延续前文TaskPhase、TaskSnapshot和revision模型,新增的是故障来源与展示策略。一个正在运行的文件整理任务,即使暂时不能显示悬浮入口,也可以继续向任务仓库发布进度;而缺少必要文件授权的图片备份任务需要暂停在“等待用户选择资源”的阶段。故障分类越清楚,恢复入口越容易保持准确。
| 条件 | 对业务任务的影响 | 界面处理 |
|---|---|---|
| 当前窗口浮层不可用 | 不自动改变任务阶段 | 收起轻量入口,显示任务中心入口 |
| 页面UI上下文已失效 | 停止该页面的界面订阅 | 由新页面重新订阅最近快照 |
| 用户未选择所需图片 | 资源准备不完整 | 提示选择图片,保留已有进度 |
| 暂时无法访问网络 | 同步步骤需要等待 | 保留断点并提供重试 |
| 本地文件已被移走 | 断点资源无效 | 说明缺失资源,要求重新选择 |
| 旧异步请求迟到 | 可能污染新状态 | 依据任务ID与版本丢弃旧结果 |
这里特别要区分应用窗口内的浮层与跨应用显示的系统级窗口形态。ArkUI公开的OverlayManager用于在当前窗口内的页面之上展示自定义UI,层级还位于对话框等组件之下;它适合作为轻量任务入口的应用内承载方式。更广范围的窗口展示需依据目标设备、公开能力和具体权限要求另行设计,不能把当前窗口内浮层理解成对所有应用窗口都具有相同覆盖范围。
二、把故障转换成稳定的降级决策
业务层先不处理具体UI对象,而是把输入事实归一为一个决策。任务的运行阶段继续交给执行仓库;显示策略只回答“在哪里让用户继续操作”。这能避免在窗口创建失败的回调中随意修改RUNNING、PAUSED等任务阶段。
export enum SurfaceFault {
NONE = 'none',
HOST_UNAVAILABLE = 'host_unavailable',
RESOURCE_REQUIRED = 'resource_required',
TRANSIENT_NETWORK = 'transient_network',
RESOURCE_MISSING = 'resource_missing'
}
export enum SurfaceTarget {
OVERLAY = 'overlay',
TASK_PAGE = 'task_page'
}
export class SurfaceDecision {
constructor(
public target: SurfaceTarget,
public message: string,
public requiresAction: boolean
) {}
}
export function decideSurface(fault: SurfaceFault): SurfaceDecision {
switch (fault) {
case SurfaceFault.NONE:
return new SurfaceDecision(SurfaceTarget.OVERLAY, '', false)
case SurfaceFault.RESOURCE_REQUIRED:
return new SurfaceDecision(SurfaceTarget.TASK_PAGE,
'选择需要处理的图片后继续', true)
case SurfaceFault.TRANSIENT_NETWORK:
return new SurfaceDecision(SurfaceTarget.TASK_PAGE,
'网络暂时不可用,可稍后重试', true)
case SurfaceFault.RESOURCE_MISSING:
return new SurfaceDecision(SurfaceTarget.TASK_PAGE,
'原始文件位置发生变化,请重新选择', true)
default:
return new SurfaceDecision(SurfaceTarget.TASK_PAGE,
'轻量入口暂不可用,请在任务中心操作', false)
}
}
SurfaceDecision属于展示策略,任务数据不会因为切换到TASK_PAGE就清零。用户下一次进入任务中心时,仍能看到最近进度、失败原因和可以采取的操作。对偶发网络错误,界面可以提供重试;对资源已失效的情况,应该先要求重新选择正确文件,避免重复执行同一个已经无法完成的步骤。
三、在当前窗口内管理轻量入口
使用ComponentContent创建浮层节点,并交给当前UIContext关联的OverlayManager管理。为了防止重复创建,控制器必须记录已经添加的节点;在页面离开时移除节点,避免返回页面后出现两份球态。创建失败要能回退到普通任务中心按钮,清理时也要让引用及时失效。
import { ComponentContent } from '@kit.ArkUI'
class BubbleArgs {
constructor(public label: string, public percent: number) {}
}
@Builder
function TaskBubble(args: BubbleArgs) {
Column() {
Text(args.label).fontSize(12).fontColor(Color.White)
Text(`${args.percent}%`)
.fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
}
.width(86).height(86)
.justifyContent(FlexAlign.Center)
.backgroundColor('#1668DC')
.borderRadius(43)
.margin({ top: 120, left: 16 })
}
@Entry
@Component
struct TaskCenter {
@State private notice: string = '任务中心已就绪'
private bubble?: ComponentContent<BubbleArgs>
private openBubble(): void {
if (this.bubble) {
return
}
const context = this.getUIContext()
const content = new ComponentContent(context,
wrapBuilder<[BubbleArgs]>(TaskBubble),
new BubbleArgs('文件整理', 67))
try {
context.getOverlayManager().addComponentContent(content, 0)
this.bubble = content
this.notice = '轻量入口已展开'
} catch (error) {
this.notice = '轻量入口暂不可用,请在任务中心操作'
}
}
private closeBubble(): void {
const content = this.bubble
if (!content) {
return
}
this.bubble = undefined
this.getUIContext().getOverlayManager().removeComponentContent(content)
}
aboutToDisappear(): void {
this.closeBubble()
}
build() {
Column({ space: 18 }) {
Text('任务中心').fontSize(26).fontWeight(FontWeight.Bold)
Text(this.notice)
Button('打开轻量入口').onClick(() => this.openBubble())
Button('收起入口').onClick(() => this.closeBubble())
Text('文件整理:67%')
}.padding(24).width('100%')
}
}
这一段展示的是当前应用窗口内的轻量UI。实际任务中心需要将67%改为订阅到的真实任务投影,并把打开入口的动作交给统一表面控制器。在跨页面导航时,应由更长生命周期的管理者持有UI策略;页面自身仍须保证订阅和浮层节点能成对释放。窗口生命周期也要独立跟踪,不能仅凭UIAbility进入前台就推断每个辅助窗口都已显示。

四、权限处理从用户意图出发
以图片备份任务为例,用户主动选择图片是比反复申请大范围媒体访问更贴近需求的入口。可以使用PhotoViewPicker让用户确定本次处理的资源;选择结果中的URI交给资源适配器。只有拿到有效URI,任务才能从等待资源阶段转入准备执行。用户取消选择时应保持当前状态,允许稍后再试,而不是把取消操作当成系统故障。
import { photoAccessHelper } from '@kit.MediaLibraryKit'
export class UserPhotoSelection {
async selectOne(): Promise<string | null> {
const options = new photoAccessHelper.PhotoSelectOptions()
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.maxSelectNumber = 1
const picker = new photoAccessHelper.PhotoViewPicker()
try {
const result = await picker.select(options)
return result.photoUris.length > 0 ? result.photoUris[0] : null
} catch (error) {
return null
}
}
}
获取URI后仍应检查资源是否可读、格式是否符合处理要求,并将资源与任务ID关联。用户选择的某个URI在后续阶段失效时,任务进入等待资源或明确的错误状态;下一次选择成功后再继续。需要其它运行时权限的具体功能,必须依照所使用Kit的权限声明与授权回调处理,避免在每次页面刷新时自动触发授权流程。任务中心应说明权限用途,并为拒绝后的操作提供清晰出口。
五、故障恢复以任务版本为准
上篇引入了恢复中间阶段RECONCILING,本篇把异常入口与执行状态连接起来:前台恢复时先向执行仓库查询当前任务事实,再决定是否显示球态、展开窗口或降级后的任务页面。不能先依据旧快照把任务直接标记为正在执行,再去等待查询结果。对每次恢复分配递增的epoch,迟到的错误回调只能影响它所属的那次操作。
export class RecoveryGate {
private epoch: number = 0
async reconcile(taskId: string,
query: (taskId: string) => Promise<number>): Promise<number | null> {
const current = ++this.epoch
try {
const progress = await query(taskId)
if (current !== this.epoch) {
return null
}
return Math.max(0, Math.min(100, progress))
} catch (error) {
if (current !== this.epoch) {
return null
}
throw error
}
}
invalidate(): void {
this.epoch++
}
}
进度读取结果需要配合任务仓库的revision一起使用。上面返回的数字只代表当前查询给出的进度,不能独立决定任务属于DONE、RUNNING还是PAUSED。例如100%的上传字节进度之后,服务端仍可能进行确认步骤,必须等待任务服务的阶段事件才能发布完成态。业务事件与页面展示事件保持分离,才能在窗口异常时保护任务的事实记录。
六、给用户留下可预测的恢复操作
任务入口消失时,最重要的是让用户知道从哪里继续。可在任务中心固定展示三个层级的信息:任务名称与最近进度、当前障碍、下一步按钮。窗口浮层不可用时显示“进入任务中心”;图片资源不足时显示“选择图片”;网络恢复前显示“重试同步”。这些按钮必须指向不同命令,不能都简单地调用“重新开始任务”。

建议用一张回归矩阵检查这条降级链路:
| 触发场景 | 期望结果 | 排查位置 |
|---|---|---|
| 快速连续点击打开球态 | 只有一个浮层节点 | ComponentContent持有者 |
| 关闭页面后重新进入 | 旧节点释放,新页面可重新创建 | aboutToDisappear与订阅管理 |
| 当前窗口浮层创建失败 | 任务中心仍可操作 | SurfaceDecision |
| 用户取消图片选择 | 保留任务及已完成进度 | 资源适配器 |
| 先失败的恢复请求迟到 | 不覆盖后续成功查询 | epoch与revision |
| 资源路径失效 | 显示重新选择入口 | 断点校验 |
还要区分“处理失败”与“显示失败”。真正的任务异常应由执行仓库决定重试、暂停或终止;显示层异常只改变用户的操作入口。这样任务即使经历前后台变化、窗口销毁或资源重新选择,也不会因为某一个UI控件失效而发生进度回退。
七、工程收束
异常降级的价值不在于隐藏所有错误,而是让错误转化为下一步可执行的操作。统一状态仓库维持任务事实,表面控制器决定球态或窗态是否可用,资源适配器负责用户选择的文件,恢复网关过滤迟到结果。对使用OverlayManager的应用内轻量入口,应遵守当前窗口的层级和生命周期;对媒体文件应选择与业务目的匹配的授权路径。下一篇将收束到高频状态刷新、批量任务投影与渲染开销,在保证状态正确的前提下优化交互平滑度。
更多推荐

所有评论(0)