目前引用计数是针对动态下载的资源,跟非动态下载的资源不太一样。
然鹅,这地方牵扯了一个比较让人头疼的问题:
预制体上图片的引用计数永远是1,不管你实例化多少份。
但如果你继续加载里了(与预制体上)相同的图片,而要管理这个图片的引用计数的时候,就出问题了,你会发现你不太好管理这个次数了。
因为纹理可以被覆盖,所以你需要在覆盖前让sprite.spriteFrame.delRef(),赋值之后再addRef()。如果只涉及到动态资源本身的引用计数,这地方不会有问题,但你遇到预制体上挂的图片跟你加载的图片一样的时候,你就蛋疼了。
你或许会说,可以预制体上不挂图片呀。嗯,你自己做的到,你能保证所有人都做得到么?
而且你会发现你没有什么好的措施去防止你的小伙伴不小心这么做。
而且你也没法保证过去不动态加载的图片后面也不动态加载。
咋整?
不知道我理解得对不对,我觉得你是在纠结动态加载一张图片赋值给 Sprite 之后,然后又用另一张图片覆盖之后又要 addRef 和 delRef
这其实是把资源管理的粒度想得太小了,实际是不需要的,一般是把加载和使用这两个行为分开想,针对一个模块进行资源释放,这个模块粒度可以小到一个预制体,比如说:
1.打开结算界面预制体 Result 的时候动态加载了一张头像图片,然后 addRef
2.期间不断使用或者用其他图片更换这张头像图片,都不会做任何额外操作
3.结算界面关闭,调用 delRef 释放这张动态加载的图片
所以除非你想的是每次替换图片都要立即释放这张图片,否则不用每次使用都维护引用计数,只需要以一个模块维护即可
只要是引用计数方式管理资源,都需要自己维护好每个计数。
使用时必须保证每个人都注意动态加载时的引用计数,这的确是有使用成本的。
额外还有一个是引擎自带了以场景为粒度的自动资源释放,可以说完全没有使用成本
勾上场景的自动资源释放后,在场景之间切换的时候会自动释放资源,而且由于里面巧妙的设计,根本不需要调用 addRef 和 delRef,可以试一试
有不少游戏是单场景的,尤其是中小游戏。。。
动态加载了一张图片,这张图片正好跟预制体上的图片一致,你要不要addRef()呢?肯定不能直接addRef是吧,除非你先delRef了一下,对冲掉这个操作。但这么操作不行啊,引擎内部所有实例化出来的相同的预制体引用次数只有1,你delRef了其中一个,这图片的引用次数就清零了,然后就被回收了。然后你就看到其他预制体上这张图片就莫名其妙黑屏了,因为它被回收释放了。
不用管预制体是否引用这张图片,引擎会维护好,只需要维护你通过 cc.assetManager 加载进来的资源的引用。
假设以我上面所说的情况,预制体引用了图片 A,然后预制体打开时(onLoad)会额外动态加载图片A 并 addRef ,在销毁(onDestroy)时对图片A delRef 。
这个预制体是挂在场景上的,比如场景上挂了10个一样的预制体,多个实例不会增加计数,所以图片 A 引用计数为 1
然后10个预制体打开时都对动态加载的图片 A 进行了 addRef ,这时候图片 A 引用计数 11
然后销毁10个预制体,10个预制体都进行了 delRef,在最后一个预制体执行 delRef 后计数变为 1,这时候只剩下预制体对图片 A 还有 1 次引擎维护的计数,这时候切换场景就释放了图片 A
假设这个预制体也是你通过 cc.assetManager 加载进来的,那么结果还是一样的:
通过 cc.assetManager 加载预制体,然后 addRef,这时候预制体引用的图片 A 计数为1,预制体本身引用计数为 1
你实例化十个这样的预制体加入到场景中,10个预制体打开时都对动态加载的图片 A 进行了 addRef ,这时候图片 A 引用计数 11
然后你不再需要这个预制体,所以你销毁这10个预制体,10个预制体都进行了 delRef,在最后一个预制体执行 delRef 后图片 A 计数变为 1,再对这个预制体 delRef,预制体包括图片 A 都正确释放完毕。
我这段描述有问题,不好意思可能误导到部分人了,引擎的静态引用不会去重,要是场景上挂了十个预制体,预制体和图片 A 的引用都会是 10,但释放的结果是不会变的,场景切换的时候会相应扣除 10 个引用计数。
你的前提条件是所有人都会遵循1对1增减
sprite的图集被覆盖了多次的情况,每次覆盖是否要做增减呢?
肯定是要做增减操作的,
考虑下 (预制体上的)纹理A->覆盖为纹理B->再次覆盖为纹理A的流程
现在这块图片的计数次数是1,不是你期望的10,你试一下
抱歉,再次错误,是的,预制体引用是 10,图片引用是 1,这次不会再错了,引擎只对直接引用的资源做计数操作
手动管理的时候也是有问题的,你不可能控制的了所有的状态。就算是一个类里你统计的了,几百上千的类里,你咋整?肯定要统一处理吧?(小规模不需要引用计数,规模大了之后才需要是吧)
谁有成熟的方案大家可以讨论一下
对,我上面也说过
只要是引用计数方式管理资源,都需要自己维护好每个计数。
使用时必须保证每个人都注意动态加载时的引用计数,这的确是有使用成本的。
就像同步锁一样,如果不能保证锁上之后会解锁,那就不要使用这个功能,但这其实不是什么难事
对于 JavaScript ,这样的引用计数的确是现在仅能做到的比较好的方式了
在引擎实现之前我们项目的一套引用计数释放就是按模块进行资源管理,你只要把粒度放大一点就可以了,我们没法做到使用时维护计数,就退一步针对一个更大的生命周期做维护计数的工作
几百上千个类就算是小项目也不难出现,针对某个类的粒度进行资源释放是不现实的,也不会这么去统计。
我们的项目有一百多个类组成的一个玩法,可以理解为一个模块,难道模块里每个类加载资源都 addRef 和 delRef?不会的,而是抽象一个“玩法资源管理类”的东西,调用这个类的函数进行加载资源,类里面进行 addRef 然后用数组记录加载过的资源,在玩法结束后,只需要对类中记录加载过的资源进行 decRef 即可
你这种思路跟引擎给你提供引用计数接口期望用法没什么关系了,完全可以在我开始的时候记录用了什么资源,最后干掉这个模块的时候疯狂的调用delRef()一万次就行了,跟实际剩几个引用一点关系都没有。
如果这样,引擎提供的这对接口还有什么意义?
而且实际开发的时候有几个会跟你的游戏完全一致,能做到按模块释放的?我游戏资源消耗多的时候,我得按粒度小得多的UI去释放。
…
要是有模块 A 和 B 都引用了同一个资源,A 模块随意去 decRef ,这个资源不就可能被提前释放吗?
引擎封装我现在能感觉到的有两个点:
1.对于新手或小项目,提供基于场景粒度的自动释放,使资源管理更简单易上手
2.对于需要以更小粒度释放的项目能更快更简单地封装自己的资源管理器,用旧引擎接口和新引擎接口封装一个就知道了
有多少游戏能做到我不太清楚,我们讨论的是:
1.引擎对这块的设计
我的观点是设计没有问题,降低了资源释放的门槛,但还是手动引用计数,所以在需要更小粒度去做资源释放的时候没有减少多少困难。
2.多少粒度适合
我的观点是按模块释放的粒度应该适合大部分游戏,而不是“有几个会一致”,一个 UI 界面也可以认为是一个模块,那看项目的不同。并且我认为绝大部分游戏都不需要以每个资源这种粒度去释放资源,但这不是选择题,我们的项目针对某些资源也会以单个资源的粒度去释放,都是视实际情况而定
我想着啊,加载资源遵循以下原则
谁加载谁释放。
加载的资源手动addRef,引用资源靠引擎addRef。
例如加载prefabA,prefabA引用了textureB,游戏逻辑中prefabA调用addRef,
之后又单独加载textureB,游戏逻辑中textureB调用addRef。
这样prefabA的ref为1 textureB的ref为2;
释放一律使用decRef自动释放,不使用release.
这样加载prefabA的模块释放时prefabA.refCount–,为0,释放。textureB.refCount–,为1不释放
在场景拖放一个精灵a,引用了texture b, 此时你去打印b的引用计数却是0,而不是1, 只有a的spriteframe的引用计数才是1,有点不理解了,按说a的精灵帧和精灵帧使用的贴图资源的引用计数不应该都是1嘛?
如果每个项目都能以模块为单位释放,那使用旧版本的资源释放方式就行了,根本不用引用计数。