茶器艺科智造HarmonyOS应用实战-54-切到商城后WebGL动画是否还在跑:用可见性协议暂停requestAnimationFrame
茶器艺科智造HarmonyOS应用实战-54-切到商城后WebGL动画是否还在跑:用可见性协议暂停requestAnimationFrame
茶杯 3D 页面启动后会递归调用 requestAnimationFrame,每帧更新 OrbitControls 并渲染 Scene。主页面切到商城时,智研 Tab 不再进入当前条件分支,但静态源码不能告诉我们 ArkWeb 此刻是销毁、隐藏、挂起,还是仍保留后台执行环境。与其猜平台会替应用省电,更稳妥的办法是定义一份 ArkTS 与 Web 页都理解的可见性协议:可见时只启动一个 RAF,隐藏时取消 RAF,页面和文档生命周期都能触发同一状态机。

1. 实际问题:界面切走不等于脚本已经收到暂停命令
主页面以 selectedTabIndex 选择内容:0 显示智研,2 显示商城。ResearchTab() 内才包含杯体 Web 预览。条件分支改变后 ArkUI 如何处理 Web 组件的实例和脚本上下文,需要运行证据;当前页面没有向 Web 明确发送“inactive”。
@Builder
MainTabsWithPillBar() {
Stack() {
if (this.selectedTabIndex === 0) {
this.ResearchTab()
} else if (this.selectedTabIndex === 1) {
this.SliceTab()
} else if (this.selectedTabIndex === 2) {
this.ShopTab()
} else {
this.ArchiveTab()
}
}
}
因此,标题中的“是否还在跑”不能仅靠代码阅读回答。可能的平台行为包括销毁 Web、暂停不可见页面、降低 RAF 频率或继续运行;系统版本和组件生命周期都可能影响结果。本文把它作为待验证问题,并设计不依赖隐式行为的应用协议。
2. 源码定位:tick没有保存ID,也没有停止条件
rawfile/cup3d/index.html 的动画函数每次先安排下一帧,再更新控制器并渲染。源码没有保存 request ID,没有 cancelAnimationFrame,也没有 document.visibilitychange、pagehide 或 native active 标志。
function tick() {
requestAnimationFrame(tick)
controls.update()
renderer.render(scene, camera)
}
tick()
这段可以证明“页面加载完成后启动递归 RAF”,但不能证明切 Tab 后它仍执行。即使浏览器内核会自动节流,应用仍缺少可观测的业务状态,无法知道恢复时是否重复启动两个循环,也无法把功耗问题与平台调度区分开。

3. 先定不变量:任意时刻最多一个RAF循环
可见性控制的核心不变量有四条:
- nativeActive 和 documentVisible 同时为 true 才渲染;
- RAF ID 非空表示已经安排下一帧,
start不得重复; - stop 先取消已安排帧并清空 ID;
- 恢复后先校正尺寸和相机,再启动一条循环。
建议把调度状态集中到一个对象,tick 在进入渲染前再次确认资格。这样 pause 与已经排队的回调相撞时,迟到回调也不会重新把循环拉起来。
var cupLoop = {
nativeActive: false,
documentVisible: !document.hidden,
rafId: 0,
frameCount: 0,
generation: 0
}
function shouldRenderCup() {
return cupLoop.nativeActive &&
cupLoop.documentVisible
}
function stopCupLoop() {
cupLoop.generation += 1
if (cupLoop.rafId !== 0) {
cancelAnimationFrame(cupLoop.rafId)
cupLoop.rafId = 0
}
}
nativeActive 初始设为 false,可以避免 Web 尚未收到宿主状态就自行常驻。若产品要求首页首帧更快,ArkTS 在 onPageEnd 后立即发送当前 Tab 状态即可。默认 true 虽然省一次消息,却会在协议丢失时退回无限循环。
4. Web侧实现:start、stop和tick形成闭环
tick 应保存当前安排的 ID;回调开始时先把 ID 清零,再检查 generation 与可见性。渲染完成后只有状态仍有效才安排下一帧。startCupLoop 在已有 ID 时直接返回,保证重复 active 消息幂等。
function startCupLoop() {
if (!shouldRenderCup() || cupLoop.rafId !== 0) {
return
}
var generation = cupLoop.generation
cupLoop.rafId = requestAnimationFrame(function frame() {
cupLoop.rafId = 0
if (!shouldRenderCup() ||
generation !== cupLoop.generation) {
return
}
controls.update()
renderer.render(scene, camera)
cupLoop.frameCount += 1
cupLoop.rafId = requestAnimationFrame(frame)
})
}
function reconcileCupLoop() {
if (shouldRenderCup()) {
onResize()
startCupLoop()
} else {
stopCupLoop()
}
}
注意 generation 在 stopCupLoop 时递增。即使 cancel 与回调执行边界相撞,旧回调也因代次不符而退出。若 controls.update() 本身存在惯性动画,暂停期间是否保留其内部状态要依据 OrbitControls 行为验证;恢复后的第一帧不应补算所有错过的时间。
5. 文档生命周期:Web自己也要听visibilitychange和pagehide
宿主 Tab 协议解决业务可见性,Web 文档事件解决应用切后台、页面缓存和内核自身可见性。两者采用 AND,而不是后到消息覆盖前一状态。否则应用后台时收到一次 native active,就可能错误恢复。
document.addEventListener(
'visibilitychange',
function () {
cupLoop.documentVisible = !document.hidden
reconcileCupLoop()
}
)
window.addEventListener('pagehide', function () {
cupLoop.documentVisible = false
stopCupLoop()
})
window.addEventListener('pageshow', function () {
cupLoop.documentVisible = !document.hidden
reconcileCupLoop()
})
pagehide 后是否还会 pageshow 取决于内核页面生命周期;监听它们是补充,不应替代 ArkTS 的 Tab 信号。页面真正销毁时,浏览器会回收上下文;协议的目标是对“仍存活但不可见”的状态给出明确行为。
6. Native协议:传active、sequence和reason
建议 Web 暴露 window.__cupSetVisibility,接收布尔值、单调 sequence 和原因。旧 sequence 直接忽略,避免切 Tab 很快时 runJavaScript 的迟到结果覆盖新状态。函数返回当前状态,便于 ArkTS 记录一次轻量回执。
var lastNativeSequence = 0
window.__cupSetVisibility = function (message) {
var sequence = Number(message.sequence || 0)
if (sequence < lastNativeSequence) {
return {
accepted: false,
sequence: lastNativeSequence,
active: cupLoop.nativeActive
}
}
lastNativeSequence = sequence
cupLoop.nativeActive = message.active === true
reconcileCupLoop()
return {
accepted: true,
sequence: sequence,
active: cupLoop.nativeActive,
running: cupLoop.rafId !== 0,
frames: cupLoop.frameCount
}
}
reason 可取 tab-change、page-appear、page-disappear,只用于诊断。不要在消息里放用户参数、纹理正文或隐私数据。协议版本也可以加入对象,防止未来 ArkTS 与缓存 HTML 结构不一致。

7. ArkTS接入:页面状态是权威输入,Web就绪后补发
CupWeb3d.ets 当前已经通过 WebviewController.runJavaScript() 调用 window.__cupSync 等函数,可以沿用这条桥。组件接收 active prop;prop 变化和 onPageEnd 都调用同步函数。同步时捕获 sequence,结果仅用于日志,不反向覆盖页面 Tab 状态。
@Prop active: boolean = false
private visibilitySequence: number = 0
private webReady: boolean = false
private async syncVisibility(reason: string): Promise<void> {
if (!this.webReady) {
return
}
const sequence = ++this.visibilitySequence
const payload = JSON.stringify({
protocol: 1,
active: this.active,
sequence,
reason
})
const script =
'window.__cupSetVisibility&&' +
'window.__cupSetVisibility(' + payload + ')'
const result =
await this.webController.runJavaScript(script)
console.info(
TAG,
'cup visibility ack',
sequence,
String(result)
)
}
若条件分支会直接销毁 CupWeb3d,切走前能否保证 prop 变化先送达需要单独验证。更可靠的入口是在父页面切换 selectedTabIndex 之前,向仍存在的 Web 发送 inactive 并等待短时回执,然后再改变分支;等待失败不能永久阻塞导航。另一选择是保留 Web 组件但设为不可见,明确用协议暂停,这会用更多内存,需权衡。
aboutToDisappear 也应发送 inactive,不能只依赖 Tab 点击。应用前后台能力若由 Ability 提供,最好由统一可见性聚合器把 pageActive、tabActive、windowVisible 合成最终 active。
8. 证明“真的停了”:计数器、性能轨迹和恢复画面一起看
Web 可暴露只读诊断快照,记录帧数、最近一帧时间、当前 active、document.hidden、RAF 是否挂起。切到商城后等待 5 秒,帧数应保持不变;切回智研后应继续增长且一次只增一条循环。计数器仅在调试模式启用日志,避免自身成为性能负担。
window.__cupGetLoopStats = function () {
return JSON.stringify({
protocol: 1,
nativeActive: cupLoop.nativeActive,
documentVisible: cupLoop.documentVisible,
running: cupLoop.rafId !== 0,
frameCount: cupLoop.frameCount,
generation: cupLoop.generation,
lastNativeSequence: lastNativeSequence
})
}
还应使用系统性能工具观察 Web 线程 CPU/GPU、帧回调与功耗趋势。帧数停止能证明应用循环停止,却不能证明 WebView 的所有线程都归零;CPU 降低也不能证明只有一条 RAF。协议读回与性能轨迹需要互相印证。
验证恢复画面时,要检查杯体参数、纹理、OrbitControls 相机位置和容器尺寸。若暂停期间参数仍变化,可选择只缓存最新 __cupSync,恢复前应用一次,避免隐藏状态反复创建几何。
9. 验证矩阵、故障排查与证据边界
| 场景 | nativeActive | documentVisible | 预期 running | 重点断言 |
|---|---|---|---|---|
| 首次打开智研 | true | true | true | 只有一个 RAF |
| 切到商城 | false | true | false | frameCount 停止 |
| 商城停留后切回 | true | true | true | 恢复且不倍速 |
| 智研时应用后台 | true | false | false | 文档事件暂停 |
| 后台期间切 Tab | false | false | false | 任一 false 都不启动 |
| 回前台但仍在商城 | false | true | false | 不被 pageshow 误启 |
| 快速 0→2→0 | 最新序列 true | true | true | 旧 inactive 不覆盖 |
| Web 尚未 ready | 消息缓存 | 未定 | false | onPageEnd 补发 |
| active 重复发送 | true×3 | true | true | RAF ID 仍只有一个 |
| 页面销毁 | false | pagehide | false | 无后续帧日志 |
| 现象 | 优先核对 | 可能原因 | 处理方向 |
|---|---|---|---|
| 商城仍有帧计数 | nativeActive 与回执 | inactive 未送达 | 切分支前发送并记录 sequence |
| 返回后动画倍速 | RAF ID 与 start 次数 | 重复启动循环 | start 幂等并保存 ID |
| 回前台自动渲染商城 | 两个可见性输入 | pageshow 覆盖了 native 状态 | 使用 AND 聚合 |
| 返回智研画面拉伸 | 恢复前尺寸 | 未调用 onResize | reconcile 时先校正 |
| 快切后状态相反 | sequence | 迟到消息覆盖 | Web 拒绝旧序列 |
| 暂停后参数丢失 | 隐藏期同步策略 | 更新被全部丢弃 | 缓存最新参数,恢复前应用 |
| 帧数停但CPU仍高 | 性能轨迹 | 还有纹理、定时器或其他 Web 任务 | 分项审计资源 |
| inactive无回执 | webReady 和函数版本 | 页面未加载或协议不匹配 | onPageEnd 补发并记录版本 |
本文已静态核对:主页面以条件分支切换智研和商城,杯体 HTML 的 tick() 无条件递归 RAF,当前没有取消 ID、可见性监听或 active 协议,ArkTS 组件已有 runJavaScript 桥。源码没有提供切到商城后的运行日志,所以本文没有断言 WebGL 一定继续跑,也没有断言平台一定会销毁或暂停。
RAF 控制器、双重可见性、sequence、ArkTS prop、诊断快照和验证矩阵都是本文建议,尚未写入参考工程。本文未运行构建、ArkWeb 页面、Tab 切换、前后台操作或性能工具。因此“暂停后节省多少 CPU/GPU/电量”“特定系统是否会自动节流”仍无证据;只有在目标设备上看到协议回执、帧计数停止、单循环恢复和性能轨迹,才能形成运行结论。
更多推荐
所有评论(0)