查看: 143|回复: 0

ArkUI纯Canvas烟花粒子系统:对象池与防丢帧调优

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在轻规划(AeroPlan)的打卡场景中,用户完成习惯后需要即时获得情绪反馈,烟花粒子特效承担了这部分交互。原文的实现没有套用WebView去跑Lottie或HTML5 Canvas,而是基于鸿蒙ArkUI原生Canvas组件自研粒子渲染系统。原因很直接:Web容器冷启动慢、内存占用大,声明式组件与Web容器之间还存在数据交换时延,跨语言边界通信也可能带来稳定性和响应迟滞问题。对于追求高帧率的打卡动画,这些代价都不合适。

一、渲染管线:绕开高频状态变更

粒子特效的本质是在高频心跳时钟下,对数百个粒子做物理位移重算、擦除和重绘。在ArkUI声明式UI中,频繁变更状态会触发UI树重排与重绘,成本较高。原文方案通过直接持有CanvasRenderingContext2D句柄,在requestAnimationFrame回调中直接操作Canvas,把物理更新和绘制放在原生绘制层级,避免频繁走声明式状态机制。

组件侧使用RenderingContextSettings(true)开启反锯齿,并绑定CanvasRenderingContext2D。Canvas的onReady回调只做初始化确认。真正的帧循环由requestAnimationFrame驱动,对应系统VSync时钟,原文目标是120Hz刷新率下的流畅渲染。每一帧先clearRect清除上一帧残留,再倒序遍历粒子数组:存活粒子执行update和draw,死亡粒子从数组移除并回收到对象池。粒子全部消亡后注销动画循环,避免空转占用CPU。
  1. private loop = () => {
  2.   this.ctx.clearRect(0, 0, this.ctx.width, this.ctx.height);
  3.   for (let i = this.particles.length - 1; i >= 0; i--) {
  4.     const p = this.particles[i];
  5.     const isAlive = p.update();
  6.     if (isAlive) {
  7.       p.draw(this.ctx);
  8.     } else {
  9.       this.particles.splice(i, 1);
  10.       ParticlePool.recycle(p);
  11.     }
  12.   }
  13.   if (this.particles.length > 0) {
  14.     this.animationId = requestAnimationFrame(this.loop);
  15.   } else {
  16.     this.animationId = -1;
  17.     console.info('HapticCanvas', 'All particles died. Animation loop paused.');
  18.   }
  19. }
复制代码

这里倒序遍历是为了在移除死亡粒子时避免数组索引偏移。循环是否继续由粒子队列是否为空决定,这也是控制功耗的一个关键检查点。

二、粒子物理模型与绘制参数

每个烟花碎片都是一个FireworkParticle。原文采用半隐式欧拉积分近似模拟运动:先按摩擦系数衰减速度,再叠加垂直重力,最后更新位置并衰减透明度。核心参数包括重力加速度0.18、空气阻力0.96,以及每帧透明度衰减值decay。粒子reset时随机生成爆炸角度0到2π、初速度4到12像素/帧,并把Y轴初速度减去2,形成向上喷射后自然炸裂的弧度。粒子半径随机在2到5像素之间,decay随机在0.012到0.027左右,以增强碎片层次感。

绘制时先save保存Canvas状态栈,设置globalAlpha和fillStyle,再beginPath、arc、fill绘制圆形粒子,最后restore恢复状态。触发一次烟花时,原文从暖红、橙色、明黄、嫩绿、蔚蓝、魅紫等颜色中随机取色,单次引爆120个粒子,在视觉饱满度和CPU负载之间取平衡。

从原文实现可以推导出,性能关键不只是Canvas绘图API本身,而是把高频更新从ArkUI状态机制中剥离,并控制对象分配与Native绘制指令数量。

三、防丢帧重点:对象池复用粒子

打卡烟花引爆瞬间,如果直接创建120个FireworkParticle实例,短时间内大量new小对象会迅速填满ArkTS虚拟机的新生代内存空间,触发Minor GC。原文指出,Minor GC虽然耗时短,但在120Hz高刷屏下,任何超过帧预算的主线程停顿都可能破坏VSync对齐,造成画面撕裂或顿挫,并影响互动响应度INP。

解决方案是粒子对象池。池容量上限设为300,obtain时优先从池尾pop并调用reset复用,池为空时才降级new;recycle时只在池未满时push,避免闲置对象常驻导致内存膨胀。原文给出的结果是,引入对象池后内存抖动波动幅度减小了90%,连续引爆多场烟花时丢帧率为0%。
  1. export class ParticlePool {
  2.   private static pool: FireworkParticle[] = [];
  3.   private static MAX_POOL_SIZE = 300;
  4.   public static obtain(x: number, y: number, color: string): FireworkParticle {
  5.     if (this.pool.length > 0) {
  6.       const p = this.pool.pop()!;
  7.       p.reset(x, y, color);
  8.       return p;
  9.     }
  10.     return new FireworkParticle(x, y, color);
  11.   }
  12.   public static recycle(p: FireworkParticle) {
  13.     if (this.pool.length < this.MAX_POOL_SIZE) {
  14.       this.pool.push(p);
  15.     }
  16.   }
  17. }
复制代码

四、进阶优化:批量绘制与离屏缓冲

对象池解决的是内存碎片和GC抖动问题。当同屏粒子达到500以上时,逐个粒子调用ctx.arc、ctx.fill等绘图指令会带来较高的Native桥接调用开销。原文给出的进一步手段是按相同颜色分组做批量绘制:同一颜色组的粒子合并到同一个Path中,一次性fill,减少Canvas状态切换和Native渲染指令投递频次。

另一个方向是OffscreenCanvas离屏双缓冲。把物理坐标计算和图形绘制放到后台或内存中预处理,再通过drawImage把离屏图像一次性合成到主屏Canvas,减少在主屏画布上做大量零散像素修改带来的渲染压力。原文将这类做法视为游戏开发中常用的优化法则,适合粒子数量更多的极端场景。

整体来看,这套方案的核心取舍是:用原生Canvas和VSync帧循环换掉WebView容器,用直接绘制换掉高频声明式状态更新,用对象池控制GC抖动,再用批量绘制和离屏缓冲压低Native绘制开销。对于HarmonyOS/ArkUI上的高频粒子动效,这些调优点比单纯堆视觉参数更值得优先验证。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-23 10:13 , Processed in 0.048753 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部