深入理解内存释放
内存释放到底在释放什么?
-
内存/显存销毁是调用gl.deleteTexture(this._glID);、image.close()等相关函数 -
解除引用关系,本质上是要回收
js object占用的内存,creator文档上也有写明需要这么做sprite.SpriteFrame = null
相对纹理的内存占用,CC.Assets的js object占用的内存量是很小的。
实现思路
addRef/decRef 的时机
以Sprite组件为切入点,对sprite.spriteFrame=xxx的get/set进行修改,增加addRef/decRef的操作
核心思路就是监控SpriteFrame相关的赋值操作
- 对旧的
texture.decRef - 对新的
texture.addRef Sprite在destroy的时候decRef
事实上cocos2dx也是这么玩的
需要注意的是在资源被释放之后,再次使用的时候需要去走load相关接口进行加载
在cocos2dx上是每次都从cache中获取,cache如果没有会加载,因为是native,所以会重新从磁盘上读取纹理文件
更智能的内存动态释放
当某个纹理超过一定帧数之后,仍旧没有被再次使用,那么我们就认为这个纹理可能需要释放,这种设计更加符合使用的直觉。
在实现的过程中,没有被使用并不是没有被渲染
比如某个Sprite暂时隐藏了,此时Sprite.texture是没有被渲染的,如果期间texture被释放了,当Sprite显示时就会发生异常。
所以在实现没有被使用的逻辑时,还需要关注这个情况。
设置纹理的渲染帧数据
要在Texture2D上挂latestFrame,不能在CC_Texture2D上,因为
_setPassProperty (name, value, pass, directly) {
let properties = pass._properties;
if (!properties.hasOwnProperty(name)) {
this._createPassProp(name, pass);
}
let prop = properties[name];
let compareValue = value;
if (prop.type === enums.PARAM_TEXTURE_2D) {
compareValue = value && value.getImpl(); // 看这里: getImpl的缘故
}
}
如果挂在CC_Texture2D上,也就是cc.Assets上,要改动的地方比较大,因为在渲染时,提交的类型是Texture2D,
- 一种做法是将
Texture2D增加一个属性指向CC_Texture2D,然后在渲染时,更新CC_Texture2D的渲染帧,这么做需要考虑引用带来的js object内存释放问题 - 另外一种做法就是顺势而为,也就是上边提到的,只需要不同的
cc.Asssets提供getLatestRenderFrame即可,不同Assets实现去Texture2D查找latestFrame即可,这其实也很容易办到,因为SpriteFrame很容易知道当前使用的纹理数据
至于获取当前游戏第几帧率,creator已经提供了这样的接口
const total = cc.director.getTotalFrames();
潜在的问题
在 C 与 C++ 等语言中,开发人员可以直接控制内存的申请和回收,而 JavaScript 所有对象的内存都由垃圾回收机制来管理,会周期性对那些我们不再使用的变量、对象所占用的内存进行释放,这就导致 JS 层逻辑永远不知道一个对象会在什么时候被释放
想象一种情况,当你释放了 AssetManager 对某个资源的引用之后,由于考虑不周的原因,游戏逻辑再次请求了这个资源,这时垃圾回收还没有开始(垃圾回收的时机不可控)
当出现这个情况时,意味着这个资源还存在内存中,但是 AssetManager 已经访问不到了,所以会重新加载它,就会造成这个资源在内存中有两份同样的拷贝,一份为刚刚请求的,另一份为已经释放但未被回收的,形成资源在内存中 暂时性 的 冗余
之所以说暂时性,是因为在下个 GC 周期时,该资源依然会被回收,释放对应的内存
如果只是一个资源还好,但是如果类似的资源很多,甚至不止一次被重复加载,就会造成当前时间内存飙升,而且频繁GC也会影响游戏的流畅性
因此我们释放资源时,应该 避免频繁释放,同时 避免释放近期内将要复用的资源