茶器艺科智造HarmonyOS应用实战-54-切到商城后WebGL动画是否还在跑:用可见性协议暂停requestAnimationFrame

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

WebGL可见性协议主题封面

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.visibilitychangepagehide 或 native active 标志。

function tick() {
  requestAnimationFrame(tick)
  controls.update()
  renderer.render(scene, camera)
}
tick()

这段可以证明“页面加载完成后启动递归 RAF”,但不能证明切 Tab 后它仍执行。即使浏览器内核会自动节流,应用仍缺少可观测的业务状态,无法知道恢复时是否重复启动两个循环,也无法把功耗问题与平台调度区分开。

Tab状态、文档可见性与RAF启停流程

3. 先定不变量:任意时刻最多一个RAF循环

可见性控制的核心不变量有四条:

  1. nativeActive 和 documentVisible 同时为 true 才渲染;
  2. RAF ID 非空表示已经安排下一帧,start 不得重复;
  3. stop 先取消已安排帧并清空 ID;
  4. 恢复后先校正尺寸和相机,再启动一条循环。

建议把调度状态集中到一个对象,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-changepage-appearpage-disappear,只用于诊断。不要在消息里放用户参数、纹理正文或隐私数据。协议版本也可以加入对象,防止未来 ArkTS 与缓存 HTML 结构不一致。

ArkTS宿主、ArkWeb协议与RAF控制器结构图

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. 验证矩阵、故障排查与证据边界

场景nativeActivedocumentVisible预期 running重点断言
首次打开智研truetruetrue只有一个 RAF
切到商城falsetruefalseframeCount 停止
商城停留后切回truetruetrue恢复且不倍速
智研时应用后台truefalsefalse文档事件暂停
后台期间切 Tabfalsefalsefalse任一 false 都不启动
回前台但仍在商城falsetruefalse不被 pageshow 误启
快速 0→2→0最新序列 truetruetrue旧 inactive 不覆盖
Web 尚未 ready消息缓存未定falseonPageEnd 补发
active 重复发送true×3truetrueRAF ID 仍只有一个
页面销毁falsepagehidefalse无后续帧日志
现象优先核对可能原因处理方向
商城仍有帧计数nativeActive 与回执inactive 未送达切分支前发送并记录 sequence
返回后动画倍速RAF ID 与 start 次数重复启动循环start 幂等并保存 ID
回前台自动渲染商城两个可见性输入pageshow 覆盖了 native 状态使用 AND 聚合
返回智研画面拉伸恢复前尺寸未调用 onResizereconcile 时先校正
快切后状态相反sequence迟到消息覆盖Web 拒绝旧序列
暂停后参数丢失隐藏期同步策略更新被全部丢弃缓存最新参数,恢复前应用
帧数停但CPU仍高性能轨迹还有纹理、定时器或其他 Web 任务分项审计资源
inactive无回执webReady 和函数版本页面未加载或协议不匹配onPageEnd 补发并记录版本

本文已静态核对:主页面以条件分支切换智研和商城,杯体 HTML 的 tick() 无条件递归 RAF,当前没有取消 ID、可见性监听或 active 协议,ArkTS 组件已有 runJavaScript 桥。源码没有提供切到商城后的运行日志,所以本文没有断言 WebGL 一定继续跑,也没有断言平台一定会销毁或暂停。

RAF 控制器、双重可见性、sequence、ArkTS prop、诊断快照和验证矩阵都是本文建议,尚未写入参考工程。本文未运行构建、ArkWeb 页面、Tab 切换、前后台操作或性能工具。因此“暂停后节省多少 CPU/GPU/电量”“特定系统是否会自动节流”仍无证据;只有在目标设备上看到协议回执、帧计数停止、单循环恢复和性能轨迹,才能形成运行结论。

Logo

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

更多推荐