两个游戏都回调“暂停”,到底是谁停了:HarmonyOS 7 GameApplicationStatusInfo 迁移实战
两个游戏都回调“暂停”,到底是谁停了:HarmonyOS 7 GameApplicationStatusInfo 迁移实战
一个调试面板同时观察两个游戏。旧代码收到“暂停”状态后,只能更新最后一次打开的游戏;结果游戏 A 暂停,面板却把游戏 B 标成暂停。日志里有时间、有状态,就是缺少能回答“这条事件属于谁”的身份字段。
HarmonyOS 7(API 26)把 gameBuddyService.onGameApplicationStatus 的回调参数从单独的 GameApplicationStatus 调整为 GameApplicationStatusInfo。新对象同时携带 gameBundle 和 status,状态终于可以按应用隔离。本文交付迁移适配器、按游戏路由的状态仓库、两种不同案例和错误码验收表。

图中左侧展示旧回调的根本歧义:两个游戏都可能产生同一个状态值。中间的新事件对象把身份和状态放进同一条消息,右侧才能稳定路由;红色分支则提醒我们,服务错误不能继续伪装成正常状态。
官方依据与版本边界
官方变更页:https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/js-apidiff-graphicsacceleratekit-7003
- API 26 新增
GameApplicationStatusInfo接口。 GameApplicationStatusInfo.gameBundle的类型为string,用于携带游戏应用标识。GameApplicationStatusInfo.status的类型为GameApplicationStatus。onGameApplicationStatus的回调参数由Callback<GameApplicationStatus>调整为Callback<GameApplicationStatusInfo>。offGameApplicationStatus同步调整为接收Callback<GameApplicationStatusInfo>,相关状态和快照回调新增错误码1009503001。
本文依据 HarmonyOS 7.0.0 Release(API 26)Graphics Accelerate Kit 官方变更说明核对接口。状态路由和幂等解绑逻辑已做宿主侧 TypeScript 断言;当前本机 SDK 为 API 24,且没有连接 HDC 设备,因此不把示例描述为 API 26 编译通过、游戏加速服务实机回调或性能实测。接入前请在 API 26 工程和支持该服务的设备上完成编译、服务连接与回归验证。
先判断迁移影响,而不是只改类型名
如果旧代码只有一份全局 currentStatus,把参数类型改成对象仍然不够。真正需要一起迁移的是四处行为:
- 日志必须同时记录
gameBundle和status。 - 状态仓库必须按 bundle 分区,不能再保存一个全局状态。
- UI 订阅必须声明自己关注哪个 bundle。
- 注册和解绑必须使用同一个回调引用,避免页面退出后继续收事件。
这也是为什么本次变更不该用 (info as any).status 草草绕过。类型错误消失了,不代表多游戏状态串线已经消失。
两种订阅架构与迁移代价
本篇属于 Graphics Accelerate Kit 回调迁移系列的“身份路由与退订”一环。只监控一个已知游戏的页面,可以采用页面级订阅并在销毁时传入同一个回调引用解绑;路径短、状态局部,但多个页面同时监控时会重复注册和维护多份快照。
多游戏调试面板更适合应用级 GameStatusRouter:平台只注册一个回调,按 gameBundle 分区后向页面分发,页面销毁只移除自己的业务订阅。它增加一层路由和生命周期管理,却避免游戏 A 的事件覆盖游戏 B,也避免页面间互相调用无参数 offGameApplicationStatus() 而误解绑对方。选择条件是订阅者数量、页面共存和是否需要跨页面保留状态,不是把旧枚举参数机械改名。
可复用的 ArkTS 事件适配器
下面的代码让平台事件只在适配器内出现,页面消费稳定的业务对象。它不猜枚举数值,也不把错误码当成某个游戏状态。
import gameBuddyService from '@hms.gameAcceleration.gameBuddyService'
import type { BusinessError } from '@kit.BasicServicesKit'
type GameStatusRow = {
gameBundle: string
status: gameBuddyService.GameApplicationStatus
receivedAt: number
}
type GameStatusListener = (row: GameStatusRow) => void
export class GameStatusRouter {
private readonly rows = new Map<string, GameStatusRow>()
private readonly listeners = new Set<GameStatusListener>()
private accepting = false
private registered = false
private registrationUnknown = false
private readonly platformCallback = (
info: gameBuddyService.GameApplicationStatusInfo
): void => {
if (!this.accepting) return
if (!info.gameBundle) {
console.error('game status dropped: empty gameBundle')
return
}
const row: GameStatusRow = {
gameBundle: info.gameBundle,
status: info.status,
receivedAt: Date.now()
}
this.rows.set(row.gameBundle, row)
this.listeners.forEach(listener => listener(row))
}
start(): void {
if (this.accepting) return
if (this.registered || this.registrationUnknown) {
throw new Error('Previous registration or unbind is unresolved; retry stop before start')
}
this.accepting = true
try {
gameBuddyService.onGameApplicationStatus(this.platformCallback)
this.registered = true
} catch (error) {
this.accepting = false
this.registrationUnknown = true
throw error
}
}
stop(): boolean {
this.accepting = false
this.rows.clear()
if (!this.registered && !this.registrationUnknown) return true
try {
gameBuddyService.offGameApplicationStatus(this.platformCallback)
this.registered = false
this.registrationUnknown = false
return true
} catch (error) {
const code = (error as BusinessError).code
console.error(`offGameApplicationStatus failed: ${code}`)
return false
}
}
get(gameBundle: string): GameStatusRow | undefined {
return this.rows.get(gameBundle)
}
subscribe(listener: GameStatusListener): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
}
platformCallback 被保存为字段,而不是注册时临时创建箭头函数。这样解绑时能传回同一个引用,也便于记录 1009503001 并停止继续假设服务可用。
案例一:多游戏调试面板按 bundle 隔离
调试面板同时展示游戏 A 和游戏 B。两者可能在几乎相同的时间进入暂停或恢复状态,但每张卡片只应响应自己的 bundle。
const router = new GameStatusRouter()
router.start()
const unsubscribe = router.subscribe((row) => {
if (row.gameBundle === this.gameABundle) {
this.gameAStatus = row.status
}
if (row.gameBundle === this.gameBBundle) {
this.gameBStatus = row.status
}
})
// 页面销毁时
unsubscribe()
router.stop()
验证时不要只看“最后状态对不对”。要把事件序号、bundle、status、接收时间和被更新的卡片 ID 放在同一行日志中。只有这样才能证明没有把游戏 A 的事件写进游戏 B。
案例二:服务断开后禁止继续展示旧状态
第二种问题不是多游戏串线,而是服务生命周期。发生 1009503001 后,如果页面继续保留上一次状态,用户会误以为监控仍在工作。
可以把连接状态和游戏状态分开保存:
type MonitorState =
| { kind: 'connected' }
| { kind: 'service-unavailable'; code: 1009503001 }
| { kind: 'failed'; code: number }
export function mapServiceError(code: number): MonitorState {
if (code === 1009503001) {
return { kind: 'service-unavailable', code: 1009503001 }
}
return { kind: 'failed', code }
}
进入 service-unavailable 后,UI 应把旧状态标记为“最后一次已知值”,停止显示实时指示灯;重新连接成功后再恢复订阅。这样不会把一次服务错误掩盖成某个游戏仍处于暂停或运行状态。
解绑失败时,本地先停止接收回调并清空实时快照,但 registered 或 registrationUnknown 会保留未解决状态,表示平台绑定情况尚未被证明。此时 start() 不得再次注册同一回调,否则会出现重复事件;可以在服务恢复后重试 stop(),确认解绑后再 start()。调用方无论 stop() 返回 true 还是 false,都要撤下页面的实时指示灯;false 还需要展示“退订未确认”的诊断状态。
验收时构造三条事件序列:A/B 交错状态、解绑后迟到回调、解绑抛错后再次 start。日志至少包含 bundle、status、接收序号、接受/忽略原因和解绑结果。宿主侧可验证状态机,API 26 工程及设备才能验证平台真实回调与错误码。
从旧回调迁移的文件级步骤
- 全库搜索
onGameApplicationStatus和offGameApplicationStatus。 - 找出所有把回调参数直接赋给全局
currentStatus的位置。 - 将事件日志扩展为
gameBundle + status + receivedAt。 - 把状态存储改为
Map<string, GameStatusRow>或等价的按 bundle 分区结构。 - 确保注册、解绑使用同一个回调引用,并覆盖重复 start/stop。
- 对
1009503001建立独立服务状态,不把它折叠成业务枚举。 - 在 API 26 工程中重新编译并执行双游戏交错事件回归。
复现与验收矩阵
| 输入/场景 | 预期分支 | 可观察信号 |
|---|---|---|
| 游戏 A 状态变化 | 写入 A 的 bundle | B 卡片不变化 |
| 游戏 B 状态变化 | 写入 B 的 bundle | A 卡片不变化 |
| A、B 交错回调 | 按 bundle 分区 | 两条时间线独立 |
| 空 gameBundle | 丢弃事件 | 记录明确错误日志 |
| 重复调用 start | 幂等返回 | 只有一个平台监听 |
| 页面退出 | 解绑同一 callback | 退出后不再更新 UI |
| 错误 1009503001 | service-unavailable | 实时指示关闭、旧值标注 |
| 其他错误码 | failed | 保留原始 code,不伪装成功 |
宿主侧状态路由测试
当前环境不能直接运行 API 26 服务回调,因此先对不依赖平台的按 bundle 路由做断言。这不能替代设备验证,但能提前发现全局状态覆盖和解绑幂等问题。
type HostEvent = { gameBundle: string; status: string }
function reduceByBundle(
state: Map<string, string>,
event: HostEvent
): Map<string, string> {
const next = new Map(state)
if (event.gameBundle) next.set(event.gameBundle, event.status)
return next
}
let state = new Map<string, string>()
state = reduceByBundle(state, { gameBundle: 'game.a', status: 'paused' })
state = reduceByBundle(state, { gameBundle: 'game.b', status: 'running' })
if (state.get('game.a') !== 'paused') throw new Error('game A overwritten')
if (state.get('game.b') !== 'running') throw new Error('game B missing')
if (state.size !== 2) throw new Error('bundle isolation failed')
上线前检查表
- API 26 工程已使用新的
GameApplicationStatusInfo签名编译。 - 日志包含 bundle、status、事件序号和接收时间。
- 多游戏状态按 bundle 分区,不再写一个全局变量。
- 注册和解绑使用同一回调引用。
- 页面离开、服务重连和重复初始化均做幂等验证。
-
1009503001被记录为服务错误,不冒充游戏状态。 - 双游戏交错事件在支持服务的设备上完成回归。
小结
这次 API 变化的价值不只是“回调参数从枚举变成对象”。gameBundle 补齐了事件身份,应用才有条件把多个游戏的状态正确分流;status 保留业务状态;1009503001 则需要进入单独的服务故障路径。
迁移时同时改类型、状态仓库、日志和生命周期,才能真正解决状态串线。只改一行回调签名,往往只是让编译器安静下来。
更多推荐



所有评论(0)