尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

HarmonyOS 7 + @ohos-lottie-HSP:动画资源上下文隔离与 DOMLoaded 提交门禁【鸿蒙心迹】

HarmonyOS 7 + @ohos-lottie-HSP:动画资源上下文隔离与 DOMLoaded 提交门禁【鸿蒙心迹】 一个动画放在 entry 模块时正常移动到 HSP 共享模块后却偶尔白屏。日志显示 JSON 已读到Canvas 也已经 onReady重复进入页面后问题反而更明显。把所有异常都归到“Lottie 加载慢”会忽略两个更具体的边界资源究竟由哪个模块上下文读取以及异步 DOMLoaded 回调是否仍属于当前页面。本文用 MotionModule 演示工程和 RewardMotionPage 页面整理一条可审计的加载链。任务 ID 为 LOTTIE-HSP-0065共享模块名 library资源为 rawfile/motion/reward.json动画实例名 reward-g25画布 360×240帧率 30。修复前演示记录 load3、listeners3、blankFrames11收口后 load1、listener1、blankFrames0首帧观察值为 84ms。数据用于解释方法不声称来自真实线上用户。一、白屏发生在“文件已经读到”之后最容易误判的日志是 JSON_READY。它说明 ResourceManager 返回了字节也说明 JSON.parse 没抛异常却没有证明动画已经绑定到当前 Canvas更没有证明 DOMLoaded 回调来自当前 generation。HSP 场景多了一层资源边界。共享模块里的 rawfile 不应由调用方凭路径猜测应该用正确的 module context 读取再把解析后的 animationData 交给 Lottie。官方开源库说明也明确给出 HSP 场景下通过 createModuleContext 与 ResourceManager 读取 rawfile再以 animationData 加载的方式。第二个边界来自异步。Canvas onReady、资源读取完成和 DOMLoaded 的先后顺序会因缓存与页面切换而变化。用户快速返回再进入时第一次加载的 DOMLoaded 可能晚于第二次 Canvas 创建。如果旧回调仍执行 play它会修改已经失效的实例状态。因此 MotionModule 不把“读取成功”直接映射成“可以播放”。状态链必须经过 CONTEXT_READY → JSON_READY → CANVAS_READY → DOM_READY → PLAYING只有同一 generation 的 JSON 与 Canvas 都就绪才允许创建实例只有对应实例触发 DOMLoaded才提交 PLAYING。二、资源上下文不是一个可省略的参数entry 页面调用 getContext(this) 得到的是当前组件所属上下文。资源若实际位于名为 library 的 HSP读取动作需要创建该模块上下文。直接拼接 common/animation.json 或复用 entry 的 ResourceManager在某些工程结构下可能读到同名资源也可能找不到最危险的是它让错误依赖目录巧合。MotionModule 给每个动画定义资源描述不让页面保存路径字符串。描述包含 moduleName、rawfileName、animationName 与期望画布比例。页面只请求 reward 动画加载器负责确定上下文和字节来源。这段代码解决什么问题在 HSP 场景中用正确的模块上下文读取 rawfile并把字节转换为可交给 Lottie 的 animationData。import{common}fromkit.AbilityKitimport{util}fromkit.ArkTSinterfaceMotionAsset{moduleName:stringrawfileName:stringanimationName:string}classHspMotionReader{asyncread(host:common.UIAbilityContext,asset:MotionAsset):Promiseobject{constmoduleContexthost.createModuleContext(asset.moduleName)constbytesawaitmoduleContext.resourceManager.getRawFile(asset.rawfileName)constdecodernewutil.TextDecoder(utf-8,{ignoreBOM:true})constjsonTextdecoder.decodeToString(bytes)constvalueJSON.parse(jsonText)asobjectreturnvalue}}代码把 rawfileName 固定为 motion/reward.jsonmoduleName 固定为 library。TextDecoder 的具体调用形式应以项目所用 API 版本定义为准如果工具链提供的是回调形式就在适配层转换为 Promise不要在页面到处复制读取代码。状态从 IDLE 进入 CONTEXT_READY 后才开始 getRawFile解析成功变为 JSON_READY。JSON.parse 失败要保留匿名资源键和错误类别不记录完整文件内容。动画 JSON 可能包含业务文案或远程资源地址直接输出原文既冗长也有泄漏风险。实际工程还应校验顶层结构和资源预算。至少检查版本字段、图层数组是否存在、画布宽高是否为正并限制文件字节数。读取成功不等于内容可信尤其当动画包来自运营配置或下载缓存时。三、Canvas 就绪和 JSON 就绪是两条并行线CanvasRenderingContext2D 只有在 Canvas onReady 后才适合作为 container 传给 loadAnimation。另一方面HSP 资源读取也可能先完成。若代码把两者写成相互嵌套的回调很快会出现“资源先到时丢一次”“Canvas 重建时重复加载”等分支。更清楚的方式是维护两个槽位animationData 与 canvasReady。任一槽位变化都调用 tryCommit只有两者齐全且当前 generation 未创建实例时才继续。这个结构像一个很小的 join不需要猜测谁先完成。这段代码解决什么问题合并异步资源与 Canvas 生命周期只允许同一 generation 创建一次动画实例。classMotionGate{privategeneration:number25privatecanvasReady:booleanfalseprivateanimationData?:objectprivatecommittedGeneration:number-1begin():number{this.generation1this.canvasReadyfalsethis.animationDataundefinedthis.committedGeneration-1returnthis.generation}acceptCanvas(generation:number):void{if(generationthis.generation){this.canvasReadytrue}}acceptData(generation:number,data:object):void{if(generationthis.generation){this.animationDatadata}}take(generation:number):object|undefined{if(generation!this.generation||!this.canvasReady||!this.animationData){returnundefined}if(this.committedGenerationgeneration){returnundefined}this.committedGenerationgenerationreturnthis.animationData}invalidate():void{this.generation1}}take 只有第一次返回数据后续重复 onReady 或重复资源回调都得到 undefined。这样 loadAnimation 的调用次数从演示中的 3 收敛到 1。generation 不是动画名称的替代它负责异步提交权animationName 则用于 Lottie 实例寻址和销毁。页面退出时 invalidate旧读取任务即便返回也无法填充新槽位。这里没有假装取消底层 IO只是拒绝过期结果。如果项目需要真正取消下载应在网络层使用独立取消机制。四、DOMLoaded 是提交门禁不是普通通知Lottie 的动画解析和构建是异步的。官方 README 建议把对 AnimationItem 的操作放在 DOMLoaded 回调里。若 loadAnimation 后立刻 play在复杂资源或冷启动下就可能早于完整构建。事件监听还要求添加与移除使用同一个回调引用。内联匿名函数看起来短却难以在释放时准确 removeEventListener。重复进入页面时每个旧监听器都可能继续响应最终出现 listeners3。这段代码解决什么问题为单个动画实例保存稳定监听器引用并在 DOMLoaded 时验证 generation 后再播放。classLottieLease{privateitem?:lottie.AnimationItemprivategeneration:number-1privatereadonlyname:stringconstructor(name:string){this.namename}bind(canvas:CanvasRenderingContext2D,data:object,generation:number,isCurrent:(value:number)boolean,onPlaying:()void):void{this.release()this.generationgenerationconstonDomLoaded():void{if(!isCurrent(generation)||!this.item){return}this.item.setFrameRate(30)this.item.play()onPlaying()}this.domLoadedListeneronDomLoadedthis.itemlottie.loadAnimation({container:canvas,renderer:canvas,loop:true,autoplay:false,name:this.name,animationData:data,contentMode:Contain})this.item.addEventListener(DOMLoaded,onDomLoaded)}privatedomLoadedListener?:()void这里 autoplayfalse把播放权留给 DOMLoaded 门禁。frameRate30 是演示选择不是所有动画的最佳值。高帧率可能增加功耗低帧率可能破坏节奏应根据素材和设备验证。name 必须稳定且在当前实例范围唯一。演示使用 reward-g25将业务名与 generation 写入诊断但真正销毁时仍保存完整 name。若多个页面都叫 reward直接使用全局 lottie.destroy(‘reward’) 可能销毁另一个页面的实例。五、release 要移除监听再按名称销毁页面不可见不等于对象已经销毁。动画仍循环绘制会继续消耗 CPU/GPU旧 DOMLoaded 回调也可能在新页面出现后触发。释放路径必须与 bind 成对而且可以重复调用。官方开源库建议优先使用 lottie.destroy(name)并在页面销毁或卸载时清理动画。MotionModule 在 release 中先移除稳定回调再按 name 销毁最后清空本地引用。这段代码解决什么问题让动画释放具有幂等性避免重复页面生命周期留下监听器或 Canvas 绘制任务。release():void{if(this.itemthis.domLoadedListener){this.item.removeEventListener(DOMLoaded,this.domLoadedListener)}lottie.destroy(this.name)this.itemundefinedthis.domLoadedListenerundefinedthis.generation-1}}Componentstruct RewardMotionPage{privatelease:LottieLeasenewLottieLease(reward-g25)privategate:MotionGatenewMotionGate()aboutToDisappear():void{this.gate.invalidate()this.lease.release()}onPageHide():void{this.gate.invalidate()this.lease.release()}}release 被两个生命周期调用也不会重复持有旧对象。具体页面是否在 onPageHide 就销毁需要按产品行为决定若短暂遮挡后希望保留进度可以 pause若页面会被频繁重建且动画很小销毁更清楚。本文选择销毁是为了让资源所有权可验证。不要把 lottie.destroy() 无参数形式当作页面清理。它可能影响进程内其他动画。按 name 销毁更符合局部所有权动画管理器若确实拥有全部实例才有资格执行全局销毁。开发示意图左侧展示 HspMotionReader.ets、MotionGate.ets、LottieLease.ets 与 RewardMotionPage.ets中间标出 createModuleContext(‘library’)、animationData、DOMLoaded 和 destroy(‘reward-g25’)右侧模拟器状态为 DOM_READY任务 LOTTIE-HSP-0065底部 HiLog 显示 load1、listener1、generation25。图中界面用于解释代码关系不冒充真实 DevEco Studio 运行截图。项目验证需要以本地构建日志、设备性能数据和官方库版本为准。六、从三次 load 反推重复注册路径修复前的演示时间线有三个入口aboutToAppear 主动读取资源Canvas onReady 再调用一次 load页面恢复可见时又重放初始化。三个入口都能到 loadAnimation于是 load3、listeners3。表面上每次调用都能看到动画问题却在快速切页后出现。第一个实例还在解析第二个实例已经绑定新 Canvas第三个回调又把状态写成 PLAYING。页面最终显示哪个实例取决于异步完成顺序。收口后生命周期入口只负责向 MotionGate 提供事实资源槽位完成、Canvas 槽位完成、页面失效。真正创建实例的地方只有 tryCommit。日志中的 load1 不是通过“只调用一次”的约定得到而是 take 的幂等条件保证。定位此类问题时不要只统计页面对象数量。建议同时记录 generation、assetKey、animationName、canvasReady、jsonReady、loadCount、listenerCount、DOMLoadedAt 与 releaseReason。任意一个旧 generation 进入 PLAYING 都应报警。七、运行页展示等待关系而不是只放一个动画手机运行图时间为 07:36状态栏包含 Wi‑Fi、5G、信号和 66% 电量。页面标题 MotionModule任务 LOTTIE-HSP-0065模块 library资源 motion/reward.jsongeneration 25。状态链显示 CONTEXT_READY、JSON_READY、CANVAS_READY、DOM_READY当前为 PLAYING。画布为 360×240帧率 30首帧 84ms。红色箭头标出“JSON 与 Canvas 同代才提交”另一个红圈强调 listener 1。首帧 84ms 是演示观察值不应作为平台承诺。不同动画复杂度、缓存状态、设备负载和资源图片数量都会改变它。真正有约束力的是同代提交、一次 load 和一次 listener。页面还应区分 LOADING 与 FAILED。资源读取失败、JSON 解析失败、DOMLoaded 超时和页面失效是不同原因。用一个“动画加载失败”提示会让诊断路径重新混在一起。八、诊断页承担异步顺序的证据详情页同样显示 07:36 与 66% 电量内容改为异步时间线。状态是 CONTEXT_READY → JSON_READY → CANVAS_READY → DOM_READY → PLAYING。修复前 load 3、listeners 3、blankFrames 11修复后 load 1、listener 1、blankFrames 0。页面显示 animationName reward-g25、generation 25、firstFrame 84ms 和 release paired。红圈围住 3→1箭头指向 stale callback dropped 2。这里 dropped 2 表示两个旧 generation 回调未提交不表示底层任务一定被取消。blankFrames 的定义要明确。本文将 Canvas 已展示但目标动画尚未绘制的采样帧记为 blank frame。它适合演示加载顺序不是通用渲染质量指标。生产项目可用截图采样、首帧回调或业务占位状态建立更可靠口径。诊断数据不要包含动画 JSON 全文或远程资源 URL 查询参数。assetKey、模块名、资源相对路径和摘要足够支持定位。九、HSP 场景还要检查图片资源与混淆Lottie JSON 可能引用外部图片。官方 README 说明相关图片通常放在 rawfile 对应目录并通过 imagePath 等配置解析。HSP 中若 JSON 来自 library图片也要在同一资源边界内验证不能默认从 entry 找到。发布构建还要关注混淆。开源库文档给出了针对 ohos/lottie 的保留建议。是否需要添加规则应以当前包版本、构建配置和实际 release 构建结果为准不能因为 debug 能播放就跳过。测试至少分四组。第一组让 JSON 先完成、Canvas 后完成第二组反过来第三组在 DOMLoaded 前退出页面第四组快速进入三次只允许最后 generation25 进入 PLAYING。每组结束后 listenerCount 应回到预期值。再加入坏资源JSON 语法错误、缺失图层、图片文件缺失、moduleName 错误和 Canvas 比例不匹配。错误要在对应阶段结束不能继续创建半初始化 AnimationItem。功耗测试不要只盯首帧。页面进入后台后帧回调和 CPU 占用应下降销毁后Canvas 不再持续绘制。若产品选择 pause 而不是 destroy则恢复时要验证播放位置与 generation 仍一致。1. 资源读取需要缓存但缓存不能越过模块边界每次进入页面都读取并解析 reward.json 会增加首帧成本完全不缓存也不现实。缓存键至少包含 moduleName、rawfileName 和资源版本不能只用 reward 这个业务名。entry 与 library 中可能存在同名文件只按文件名缓存会把错误模块的数据返回给调用方。缓存保存解析后的不可变 animationData不保存 CanvasRenderingContext2D 或 AnimationItem。前者可以被多个页面读取后两者属于具体渲染实例。若把 AnimationItem 放进全局缓存页面释放时就无法判断还有谁在使用按 name 销毁也会变得危险。运营更新动画资源时版本号必须进入缓存键。旧版本解析任务晚到后generation 校验会拒绝页面提交但缓存层也不能覆盖新版本。可以让缓存写入使用 assetVersion 的条件比较只有当前版本才允许成为 latest。2. DOMLoaded 超时要结束在可解释状态事件门禁不能无限等待。JSON_READY 与 CANVAS_READY 都完成后如果在业务预算内没有收到 DOMLoaded页面应进入 DOM_TIMEOUT释放实例并展示静态占位。超时值应按素材复杂度和设备档位制定不能把演示的 84ms 乘一个固定倍数当成通用答案。超时回调同样带 generation。若第 24 代超时计时器在第 25 代已经 PLAYING 后触发它必须被丢弃否则正常动画会被旧计时器销毁。release 时清除计时器invalidate 时增加 generation两层防护缺一不可。超时日志记录当前阶段、animationName、assetKey、elapsedMs 和资源摘要不记录 JSON。下一次进入页面可以重试但要先执行 lottie.destroy(name)确保失败实例没有留在全局注册表中。3. 多动画页面要使用租约集合奖励页可能同时有背景光效、徽章和数字跳动三个动画。如果三个实例都赋给 this.item最后一次赋值会覆盖前两个引用单个 animationItem.destroy 无法清理全部实例。官方 README 也提醒了同类风险。MotionModule 为每个动画创建独立 LottieLease名称分别为 reward-bg-g25、reward-badge-g25 和 reward-count-g25。页面级 MotionLeaseSet 只做集合管理按依赖顺序 bind退出时逆序 release。任何一个 bind 失败已经创建的租约立即回滚。集合仍不能调用无参数 lottie.destroy 作为快捷方式因为进程里可能还有导航页动画。局部所有权的原则不因数量增加而改变。诊断页应显示 activeLeases3 与 releasedLeases3而不是只写“已清理”。4. Canvas 尺寸变化需要 resize而不是重新加载 JSON折叠屏、多窗口或横竖屏切换会改变 360×240 画布。只要资源与实例仍有效尺寸变化优先调用 AnimationItem.resize让渲染适配新的 Canvas不应把每次 onAreaChange 都接到 loadAnimation。resize 事件也需要合并。窗口变化期间可能连续产生多次宽高逐次 resize 会增加绘制开销。可以保存最后尺寸在一帧内提交一次。若 Canvas 被真正销毁并重建则旧租约 release新 Canvas onReady 后再由 MotionGate 建立新 generation。比例变化要有产品策略。reward.json 若按 3:2 设计画布从 360×240 变成 240×240 时Contain 会留白Cover 会裁切。contentMode 是视觉决策不应由加载器静默更改。诊断记录 sourceRatio、canvasRatio 与 contentMode方便区分“没加载”和“被裁切”。5. 网络动画和本地动画不能共用同一信任模型开源库支持 uri 方式加载网络资源并要求相应网络权限。网络动画引入了下载失败、缓存过期、重定向和内容变更不能直接套用本地 rawfile 的“读取即可信”路径。若业务允许网络动画应先在受控下载层验证域名、大小、摘要和 MIME再把本地缓存结果交给解析器。页面不直接把任意 URL 传给 loadAnimation。这样资源治理、重试和隐私审计仍在应用边界内。本文 Demo 明确使用 HSP rawfile因此 module context 是重点。把网络分支写进同一个 load 方法会模糊状态CONTEXT_READY 对本地资源有意义对网络下载却不完整。更好的做法是共享最终 MotionAssetData 接口保留不同来源的准备状态机。6. 发布前要跑 release 构建和重复进入回归debug 构建能播放不代表混淆后的 HAP 一定正常。发布前按照当前 ohos/lottie 文档核对混淆规则使用真实 release 配置打开 RewardMotionPage并检查 DOMLoaded、图片资源和 destroy 日志。重复进入测试至少执行二十轮每轮等待随机 0200ms 后返回以覆盖 JSON_READY 前、CANVAS_READY 后和 DOMLoaded 前等不同窗口。最终 active instance 为 0listenerCount 为 0下一次进入仍只有一次 load。性能报告把冷启动与热缓存分开。首帧 84ms 只属于本次演示的热身条件冷读 rawfile、首次 JSON.parse 和图片解码应分别记录。若只报告最小值会掩盖用户第一次打开页面的实际体验。十、把第三方动画库放进应用边界ohos/lottie 提供 loadAnimation、事件监听、play、pause、setFrameRate 和 destroy 等能力。它不会替应用决定资源属于哪个 HSP也不会知道哪个异步结果已经过期。这两件事必须由应用适配层管理。MotionGate 解决“何时允许创建”LottieLease 解决“谁拥有实例”HspMotionReader 解决“资源从哪里来”。页面只组合三者不接触到处散落的路径、匿名回调和全局销毁。这套结构的边界也很明确。它没有改变 Lottie 的解析性能没有保证所有动画首帧都在 84ms 内也没有替代素材质量检查。它只是把三次加载、三个监听器和旧回调提交收敛为可验证状态。团队协作时动画资产也需要一份机读清单。清单记录 assetKey、moduleName、rawfileName、设计画布、contentMode、目标帧率和资源版本代码生成或静态检查负责发现重复 animationName、缺失文件与错误模块。这样设计同学替换 reward.json 时不必依靠开发者记住所有隐含约定。错误恢复应保持克制。JSON_INVALID 不自动无限重读本地文件MODULE_NOT_FOUND 不回退到 entry 中的同名资源DOM_TIMEOUT 只允许在创建新 generation 后重试一次。静默回退虽然可能让动画“碰巧出现”却会破坏资源隔离让问题在 release 构建或另一台设备上重新暴露。安全边界同样适用于素材。若 JSON 包含外部图片地址或表达式能力接入前要按当前库支持范围和应用策略检查不要因为它是动画文件就跳过内容来源审计。第三方库负责渲染应用仍然负责决定什么资源可以进入运行链路。最终判断可以写得很克制动画白屏不总是素材坏了也不总是 Canvas 慢了。当资源跨模块、加载异步、页面可重复进入时正确上下文、同代提交和成对释放缺一不可。把 CONTEXT_READY 到 PLAYING 的每一步记录下来第三方库才不再是页面里的黑盒。参考资料核对日期2026-10-01OpenHarmony-TPC lottieArkTS READMEHarmonyOS ArkTS 运行时概述资源管理开发指导动态共享包 HSP 开发指导
返回列表