最早的构想:有很多问题
preload:load(){ if(!list.has()){ list.add() } } preload:unload(){ if(list.has()){ list.remove } } if(totalMem > maxMem){ cache:remve()// 这个不会通知preload,导致preload认为还有这个texture }
sprite:create() scene:add(sprite) sprite:remove() // 此时没有人使用这个texture setMem(10)//就会触发清理texture
preload:load(res) sprite:create() scene:add(sprite) sprite:remove() // 此时没有人使用这个texture setMem(10)//不会触发清理texture,因为preload还持有 preload:unload(res)// 真正的清理了
preload:load(res) sprite:create() scene:add(sprite) sprite:remove() // 此时没有人使用这个texture setMem(10)//不会触发清理texture,因为preload还持有 preload:unload(res)// 真正的清理了
sprite:create() scene:add(sprite) preload:load(res)// 发现是外人持有 sprite:remove() // 此时没有人使用这个texture setMem(10)//不会触发清理texture,因为preload还持有 preload:unload(res)// 无法删除,因为是外人持有,应该交给外人管理
spirteFrameCaceh frame1 texture
绕过SpriteFrameCache,导致SpriteFrameFrameCache不知道Frame的Texuter已经没了
delete texture -> check in SpriteFrameCache -> notify SpriteFrame update texture to null
垃圾回收机制
如何判定为垃圾?
- 没有人使用: 预加载后2s再使用,中间没人使用,也会被判定为垃圾,所以需要增加预加载接口,对preload assets -> retain
大致实现
schedule(()=>{ spriteFrameCache::gc();// 怎么知道frame没有人使用,因为addCache后为2? // 为2就是没人在使用,可以尝试clean textureCache::gc(); })
怎么从SpriteFrameCache中移除
让frame.ref=1
function SpriteFrameCache:gc(){ /* 怎么知道卸载哪个 SpriteFrameCache frame1 frame2 **/ if(frame.shouldUnload()){ frame1.release() 会导致一个plist创建的N个frame,并不会完全卸载, //只要spriteFrame还有在使用,对应的Texture就不能卸载,Texture对应的所有SpriteFrame都不能卸载 } // 卸载没有任何人在使用的plist,即plist的每个SpriteFrame都没人在用 spriteFrameCache.allPlist.for(plist=>{ if(plist.spriteFrames.isUnuse()){ spriteFrameCache:removeSpriteFramesFromFile(plist) // 尝试卸载plist文件 } }) // 调用这个api意味着: spriteFrameCache:removeSpriteFrameByName }
导致的问题
// 情况1 spriteFrameCache.add(plist) frame1.ref=2; // create +1 insertSpriteFrameCache +1 没人使用,也会导致被gc // 情况2 const csbNode=csb:load() csbNode:remove() // 此时csb对应的plist也没人使用了,也会导致被gc // 触发了垃圾回收 异步逻辑 const frame = spriteFrameCache:getSpriteFrameByName(name) //可能为null,原因是垃圾回收将没有人使用的spriteFrame清理了 // name是路径,所以不会出现撞车 // 但是被移除了,也不知道这个name来自哪个plist // 解决办法是记录信息,然后重新加载这个plist const sprite = Sprite::create(frame) // null
removeUnuse() 虽然从cache中被干掉了,没有被真正的delete,但是其他地方的Sprite还在使用,当这个Sprite不使用,就会调用一次release,对应的texture才会被真正释放了
- 一个极端的流程
texttureCache:add(1.png) let spr1=sprite:create(1.png) scene.add(spri1) textureCache:remove(1.png) //old 1.png没有被delete textureCache:add(1.png) // 此时使用的new 1.png纹理 let spr2= sprite:create(1.png) // 使用的是new 1.png scene.add(spr2) spr1.remove() // old 1.png会被delete,实际肯定不会这么玩
- 正常的容易维护的流程
// preload textureCache:add(1.png) let spr1=sprite:create(1.png) scene.add(spri1) spr1.remove() // unload textureCache:remove(1.png) // 1.png会被delete
因为Texture没有加入AutoRelease,所以只能定时gc,仅仅是让cache放弃持有
function gc(){ if(texture.lastUseTime < 1s && texture.ref=1){ texture.release() } }
spriteFrameCache的gc思路
因为spriteFrame的本质是Texture的某一部分,一个plist有多个SpriteFrame,但不是每个SpriteFrame都会被使用。
比如button有normal、press2个状态,切换状态时都会从SpriteFrame中取,但是button只会使用其中的某一个状态,press状态的SpriteFrame没有人在使用,但是不代表这个SpriteFrame是无用的。
只有plist对应的所有SpriteFrame.ref=1,才能说明这个plist没人在使用,才需要卸载对应的所有SpriteFrame,这样做才安全。
单个SpriteFrame需要知道和它在一个plist的所有SpriteFrames,一种做法是新建一个plistDict类,持有它对应的frames,这样也不太好,主要是它还有removeSpriteFrameByName这样的接口,可以删除某一个SpriteFrame, 所以还需要从_spriteFrames单元遍历,分析出来对应的plist引用情况
auto gc的思路
自动gc,甚至都不需要unload,如果后续有再使用,unload会导致新的重复纹理产生,old Texture因为无人使用,到达lastUseTime才会被delete,如果不unload,lastUseTime会被更新,这样就不会释放了,也无需重复生成新纹理。
基于这个原理,只需要检测出Texture的使用频率,让cache释放掉低使用频率的持有关系即可。
texture->getName()可以证明渲染使用频率,在提交渲染时,更新texture.lastUseTime
新的使用流程:不需要手动remove,只管load就行了,gc会自动处理
// 进入到场景1 textureCache:add(image1) textureCache:add(image2) let spr1 = sprite:create(image1) scene:add(spr1) spr1:remove() //进入到场景2,为了提升create性能,把image1, image2重新加一下 textureCache:add(image1) textureCache:add(image2) let spr2 = sprite:create(image2) scene:add(spr2) // 到这里只用了image2, image1当到达gc时间后,会被gc自动清理
在渲染的时候更新纹理的渲染时间
大部分使用的是TrianglesCommand,也有QuadCommand
TrianglesCommand::useMaterial(){ GL::bindTexture2D(_textureID); }
如何统计纹理内存
在Texture2D的构造函数进行统计添加,在Texture.init的时候才能真正知道纹理的占用大小,在Texture2D的析构函数进行统计移除,这样是最准确的。不能依赖TextureCache的add remove,因为removeUnuse
函数有坑,
在Texture构造函数统计,new Texture
的纹理也会被统计进去,而且这个纹理不加入cache,所以texture.ref=1就需要释放不符合这类纹理,所以texture.ref=1
只适合textureCache里面的纹理。
发现以上的逻辑的问题:如果_utf8Text
为空字符串,也会创建一个空的纹理,不过它可以正常随着label释放。
预加载对gc的影响
- button
const btnPressTexture = textureCache.load("button_press.png")// 此时press纹理的ref=1,只有cache在持有 button.setPressTexture(btnPressTexture) // 给某个状态设置了一个纹理
当button触发press状态时,就会使用press纹理,此时的这个press纹理不能被释放,否则会有问题。
如果是在press是在plist里面,还不会被gc掉,因为plist.normal在使用。
但是如果press在另外的一个plist里面,按照目前的gc规则,它的确是有可能被gc掉,追加一个gc保护的设计可以避免press纹理被释放。
- preload
// 加载大量的图集,但是就不使用,而且在这个scene有可能还真用不到 enterScene() preload(1.plit, 2.plit) use(1.plit) exitScene()
2.plist
在没有exitScene
有可能就会被释放掉
如果不追加gc保护,那么游戏在正常的运行过程中,也会发生2.plit
因为无人使用被释放的问题,但是后续会再使用2.plist
的某个SpriteFrame,所以plist的信息也需要保存起来,方式后续如果获取不到SpriteFrame的时候,再次reload
还有一种办法解决,就是texture的useFrame如果是0,表示创建后没有使用,而预加载的texture刚好是符合这种情况的,所以0是一个非常特殊的值,但是这样做会导致一个问题,如果一直不使用,texture.useFrame就一直是0,就无法被gc释放掉。
因为预加载后就会一直放在那里,可能很久一段时间之后才会突然的使用,
当然也可以检测哪段时间大量密集的加载纹理,证明这些纹理是预加载的,但是这些都是基于表现推理的结果,没有preload api准确
节点缓存对gc的影响
思考
nodePool里面的每一帧都会更新lastRenderTime,保证node对应的纹理不会被gc
因为nodePool里面的node不会被渲染,导致不能更新lastUseTime
如何知道node,及其子节点所使用的纹理?
答案
以上的顾虑是多余的,不需要更新lastRenderTime也没问题,虽然lastTime满足条件,但是texture.ref
不满足。不需要for循环遍历,判断引用计数即可,就连缓存池都不需要。
Sprite.create(1.png)
不管有没有添加到scene中,Texture对应的ref已经是2了
只有sprite.remove
或者 sprite.release
才会让Texture.ref=1
, 1
意味着只有cache持有,这时只需要cache对接下gc逻辑即可。
主动调用sprite.retain()
实现了缓存的效果,此时sprite.ref=2, 而Texture.ref=2是不会受影响的。