weakref 仅提供弱持有能力,不触发资源释放;必须配合 finalizationregistry 在 js 对象销毁时立即调用 gl.deletetexture() 并清理缓存,同时提供主动释放兜底机制。

WeakRef 本身不触发释放,只提供“弱持有”能力
WeakRef 不会阻止目标对象被垃圾回收,但它也不会主动释放任何资源——尤其是 WebGL 纹理这种驻留在 GPU 内存中的对象。JS 层的 WeakRef 只能帮你感知“JS 对象是否还活着”,而 gl.deleteTexture() 必须由你显式调用。如果只靠 WeakRef.deref() 判断再释放,大概率已经晚了:纹理 JS 包装对象可能已被 GC,但 GPU 纹理还挂着,内存泄漏已发生。
真正可行的路径是:用 WeakRef + FinalizationRegistry 捕获 JS 对象销毁时机,**立刻同步调用 gl.deleteTexture()**,并把该纹理 ID 从缓存中剔除。
- 必须在注册
FinalizationRegistry时传入原始WebGLTexture实例(不是包装类),否则回调里拿不到可删对象 - 不能依赖
deref()的返回值做释放判断——它可能返回undefined,但此时gl.deleteTexture()还没执行 - 注册时传入的
heldValue应该是纹理 ID 或纹理对象本身,方便回调中直接调用销毁 API
需要手动管理 WebGLTexture 生命周期,不能依赖 JS GC
WebGL 资源(WebGLTexture、WebGLBuffer 等)存在于 GPU 内存,不受 JavaScript 垃圾回收器管辖。即使 JS 层所有引用都断开、WeakRef 失效、FinalizationRegistry 触发回调,GPU 纹理仍会持续占用显存,直到你调用 gl.deleteTexture()。
所以“自动释放”的本质不是“等 GC”,而是“借 GC 信号做清理钩子”。关键动作必须手动写死:
- 创建纹理后,除了存入缓存 Map,还要用
registry.register(texture, textureId, texture) - 回调函数里第一行就是
if (gl.isTexture(texture)) gl.deleteTexture(texture) - 紧接着清理缓存:
cache.delete(textureId) - 务必检查
gl.isTexture(),防止重复删除或上下文丢失后调用失败
缓存键设计要兼顾查找效率与生命周期解耦
如果用原始图片 URL 当缓存键,看似直观,但容易导致同一张图被多次加载为不同 WebGLTexture 实例(比如不同 filter 或 wrap 设置),无法复用;若改用纹理配置对象(如 { url, magFilter: gl.LINEAR })作键,又得实现深比较或序列化,拖慢 get() 性能。
更务实的做法是:生成唯一纹理 ID(如 url + '_' + hash(config)),作为缓存 Map 的 key,同时把这个 ID 传给 FinalizationRegistry.register() 的 heldValue。这样既能快速查找,又能让销毁回调精准定位要删哪个纹理。
- 避免用原始
HTMLImageElement或ArrayBuffer当键——它们可能被复用或提前释放,导致键失效 - ID 字符串中不要包含时间戳或随机数,否则失去复用意义
- 如果项目用 Cocos Creator,优先走
cc.assetManager.releaseAsset(),而不是自己封装 WebGL 层释放逻辑
FinalizationRegistry 回调不可靠,必须配合主动释放兜底
FinalizationRegistry 的回调是异步且非实时的,规范不保证何时触发,甚至可能永不触发(尤其在页面卸载前)。这意味着单靠它做“自动释放”存在风险:纹理可能长期滞留 GPU,直到下一次 GC 或页面关闭。
因此,大型资源管理器必须提供主动释放入口,并在关键节点调用:
- 场景切换时,遍历当前场景使用的所有纹理 ID,逐个调用
disposeTexture(id) -
disposeTexture()内部应先gl.deleteTexture(),再cache.delete(),最后调用registry.unregister(token)避免重复回调 - 在
onDestroy或组件卸载时,不要只依赖decRef(),要显式调用资源释放方法 - Chrome DevTools 的 Memory 面板中,重点观察
WebGLTexture实例数是否随操作下降,这是验证释放是否生效的唯一可信指标
FinalizationRegistry 注册时传入的 unregister token 必须是同一个对象实例**。如果每次 set() 都新建一个 WeakRef 并传入新 token,旧 token 将无法被注销,可能导致重复回调或内存残留。这个细节没有调试工具会报错,但会让缓存清理逻辑静默失效。











