资源释放
这一小节重点介绍在Creator中释放资源的三种方式以及其背后的实现,最后介绍在项目中如何排查资源泄露的情况。
Creator的资源释放
Creator支持以下3种资源释放的方式:
释放方式 | 释放效果 |
---|---|
勾选:场景->属性检查器->自动释放资源 | 在场景切换后,自动释放新场景不使用的资源 |
引用计数释放res.decRef
|
使用addRef和decRef维护引用计数,在decRef后引用计数为0时自动释放 |
手动释放cc.assetManager.releaseAsset(texture);
|
手动释放资源,强制释放 |
场景自动释放
当一个新场景运行的时候会执行Director.runSceneImmediate方法,这里调用了_autoRelease来实现老场景资源的自动释放(如果老场景勾选了自动释放资源)。
runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
// 省略代码...
var oldScene = this._scene;
if (!CC_EDITOR) {
// 自动释放资源
CC_BUILD && CC_DEBUG && console.time('AutoRelease');
cc.assetManager._releaseManager._autoRelease(oldScene, scene, persistNodeList);
CC_BUILD && CC_DEBUG && console.timeEnd('AutoRelease');
}
// unload scene
CC_BUILD && CC_DEBUG && console.time('Destroy');
if (cc.isValid(oldScene)) {
oldScene.destroy();
}
// 省略代码...
},
最新版本的_autoRelease的实现非常简洁干脆,将持久节点的引用从老场景迁移到新场景,然后直接调用资源的decRef减少引用计数,而是否释放老场景引用的资源,则取决于老场景是否设置了autoReleaseAssets。
// do auto release
_autoRelease (oldScene, newScene, persistNodes) {
// 所有持久节点依赖的资源自动addRef、并记录到sceneDeps.persistDeps中
for (let i = 0, l = persistNodes.length; i < l; i++) {
var node = persistNodes[i];
var sceneDeps = dependUtil._depends.get(newScene._id);
var deps = _persistNodeDeps.get(node.uuid);
for (let i = 0, l = deps.length; i < l; i++) {
var dependAsset = assets.get(deps[i]);
if (dependAsset) {
dependAsset.addRef();
}
}
if (sceneDeps) {
!sceneDeps.persistDeps && (sceneDeps.persistDeps = []);
sceneDeps.persistDeps.push.apply(sceneDeps.persistDeps, deps);
}
}
// 释放老场景的依赖
if (oldScene) {
var childs = dependUtil.getDeps(oldScene._id);
for (let i = 0, l = childs.length; i < l; i++) {
let asset = assets.get(childs[i]);
asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets);
}
var dependencies = dependUtil._depends.get(oldScene._id);
if (dependencies && dependencies.persistDeps) {
var persistDeps = dependencies.persistDeps;
for (let i = 0, l = persistDeps.length; i < l; i++) {
let asset = assets.get(persistDeps[i]);
asset && asset.decRef(CC_TEST || oldScene.autoReleaseAssets);
}
}
dependUtil.remove(oldScene._id);
}
},
引用计数和手动释放资源
剩下两种释放资源的方式,本质上都是调用releaseManager.tryRelease来实现资源释放,区别在于decRef是根据引用计数和autoRelease来决定是否调用tryRelease,而releaseAsset是强制释放。资源释放的完整流程大致如下图所示:
// CCAsset.js 减少引用
decRef (autoRelease) {
this._ref--;
autoRelease !== false && cc.assetManager._releaseManager.tryRelease(this);
return this;
}
// CCAssetManager.js 手动释放资源
releaseAsset (asset) {
releaseManager.tryRelease(asset, true);
},
tryRelease支持延迟释放和强制释放2种模式,当传入force参数为true时直接进入释放流程,否则creator会将资源放入待释放的列表中,并在EVENT_AFTER_DRAW
事件中执行freeAssets方法真正清理资源。不论何种方式,资源会传入到_free方法处理,这个方法做了以下几件事情。
- 从_toDelete中移除
- 在非force释放时,需要检查是否还有其它引用,如果是则返回
- 从assets缓存中移除
- 自动释放依赖资源
- 调用资源的destroy方法销毁资源
- 从dependUtil中移除资源的依赖记录
checkCircularReference返回值如果大于0,表示资源还有被其它地方引用,其它地方指所有我们addRef的地方,该方法会先记录asset当前的refCount,然后消除掉资源和依赖资源中对asset的引用,这相当于资源A内部挂载了组件B和C,它们都引用了资源A,此时资源A的引用计数为2,而组件B和C其实是要跟着A释放的,而A被B和C引用着,计数就不为0无法释放,所以checkCircularReference先排除了内部的引用。如果资源的refCount减去了内部的引用次数还大于1,说明有其它地方还引用着它,不能释放。
tryRelease (asset, force) {
if (!(asset instanceof cc.Asset)) return;
if (force) {
releaseManager._free(asset, force);
}
else {
_toDelete.add(asset._uuid, asset);
// 在下次Director绘制完成之后,执行freeAssets
if (!eventListener) {
eventListener = true;
cc.director.once(cc.Director.EVENT_AFTER_DRAW, freeAssets);
}
}
}
// 释放资源
_free (asset, force) {
_toDelete.remove(asset._uuid);
if (!cc.isValid(asset, true)) return;
if (!force) {
if (asset.refCount > 0) {
// 检查资源内部的循环引用
if (checkCircularReference(asset) > 0) return;
}
}
// 从缓存中移除
assets.remove(asset._uuid);
var depends = dependUtil.getDeps(asset._uuid);
for (let i = 0, l = depends.length; i < l; i++) {
var dependAsset = assets.get(depends[i]);
if (dependAsset) {
dependAsset.decRef(false);
releaseManager._free(dependAsset, false);
}
}
asset.destroy();
dependUtil.remove(asset._uuid);
},
// 释放_toDelete中的资源并清空
function freeAssets () {
eventListener = false;
_toDelete.forEach(function (asset) {
releaseManager._free(asset);
});
_toDelete.clear();
}
asset.destroy做了什么?资源对象是如何被释放掉的?像纹理、声音这样的资源又是如何被释放掉的呢?Asset对象本身并没有destroy方法,而是Asset对象所继承的CCObject对象实现了destroy,这里的实现只是将对象放到了一个待释放的数组中,并打上ToDestroy
的标记。Director每一帧都会调用deferredDestroy来执行_destroyImmediate
进行资源释放,这个方法会对对象的Destroyed标记进行判断和操作、调用_onPreDestroy
方法执行回调、以及_destruct
方法进行析构。
prototype.destroy = function () {
if (this._objFlags & Destroyed) {
cc.warnID(5000);
return false;
}
if (this._objFlags & ToDestroy) {
return false;
}
this._objFlags |= ToDestroy;
objectsToDestroy.push(this);
if (CC_EDITOR && deferredDestroyTimer === null && cc.engine && ! cc.engine._isUpdating) {
// 在编辑器模式下可以立即销毁
deferredDestroyTimer = setImmediate(deferredDestroy);
}
return true;
};
// Director每一帧都会调用这个方法
function deferredDestroy () {
var deleteCount = objectsToDestroy.length;
for (var i = 0; i < deleteCount; ++i) {
var obj = objectsToDestroy[i];
if (!(obj._objFlags & Destroyed)) {
obj._destroyImmediate();
}
}
// 当我们在a.onDestroy中调用b.destroy,objectsToDestroy数组的大小会变化,我们只销毁在这次deferredDestroy之前objectsToDestroy中的元素
if (deleteCount === objectsToDestroy.length) {
objectsToDestroy.length = 0;
}
else {
objectsToDestroy.splice(0, deleteCount);
}
if (CC_EDITOR) {
deferredDestroyTimer = null;
}
}
// 真正的资源释放
prototype._destroyImmediate = function () {
if (this._objFlags & Destroyed) {
cc.errorID(5000);
return;
}
// 执行回调
if (this._onPreDestroy) {
this._onPreDestroy();
}
if ((CC_TEST ? (/* make CC_EDITOR mockable*/ Function('return !CC_EDITOR'))() : !CC_EDITOR) || cc.engine._isPlaying) {
this._destruct();
}
this._objFlags |= Destroyed;
};
在这里_destruct
做的事情就是将对象的属性清空,比如将object类型的属性置为null,将string类型的属性置为’’,compileDestruct方法会返回一个该类的析构函数,compileDestruct先收集了普通object和cc.Class这两种类型下的所有属性,并根据类型构建了一个propsToReset用来清空属性,支持JIT的情况下会根据要清空的属性生成一个类似这样的函数返回function(o) {o.a='';o.b=null;o.['c']=undefined...}
,而非JIT情况下会返回一个根据propsToReset遍历处理的函数,前者占用更多内存,但效率更高。
prototype._destruct = function () {
var ctor = this.constructor;
var destruct = ctor.__destruct__;
if (!destruct) {
destruct = compileDestruct(this, ctor);
js.value(ctor, '__destruct__', destruct, true);
}
destruct(this);
};
function compileDestruct (obj, ctor) {
var shouldSkipId = obj instanceof cc._BaseNode || obj instanceof cc.Component;
var idToSkip = shouldSkipId ? '_id' : null;
var key, propsToReset = {};
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === idToSkip) {
continue;
}
switch (typeof obj[key]) {
case 'string':
propsToReset[key] = '';
break;
case 'object':
case 'function':
propsToReset[key] = null;
break;
}
}
}
// Overwrite propsToReset according to Class
if (cc.Class._isCCClass(ctor)) {
var attrs = cc.Class.Attr.getClassAttrs(ctor);
var propList = ctor.__props__;
for (var i = 0; i < propList.length; i++) {
key = propList[i];
var attrKey = key + cc.Class.Attr.DELIMETER + 'default';
if (attrKey in attrs) {
if (shouldSkipId && key === '_id') {
continue;
}
switch (typeof attrs[attrKey]) {
case 'string':
propsToReset[key] = '';
break;
case 'object':
case 'function':
propsToReset[key] = null;
break;
case 'undefined':
propsToReset[key] = undefined;
break;
}
}
}
}
if (CC_SUPPORT_JIT) {
// compile code
var func = '';
for (key in propsToReset) {
var statement;
if (CCClass.IDENTIFIER_RE.test(key)) {
statement = 'o.' + key + '=';
}
else {
statement = 'o[' + CCClass.escapeForJS(key) + ']=';
}
var val = propsToReset[key];
if (val === '') {
val = '""';
}
func += (statement + val + ';\n');
}
return Function('o', func);
}
else {
return function (o) {
for (var key in propsToReset) {
o[key] = propsToReset[key];
}
};
}
}
那么_onPreDestroy
又做了什么呢?主要是将各种事件、定时器进行注销,对子节点、组件等进行删除,详情可以看下面这段代码。
// Node的_onPreDestroy
_onPreDestroy () {
// 调用_onPreDestroyBase方法,实际是调用BaseNode.prototype._onPreDestroy,这个方法下面介绍
var destroyByParent = this._onPreDestroyBase();
// 注销Actions
if (ActionManagerExist) {
cc.director.getActionManager().removeAllActionsFromTarget(this);
}
// 移除_currentHovered
if (_currentHovered === this) {
_currentHovered = null;
}
this._bubblingListeners && this._bubblingListeners.clear();
this._capturingListeners && this._capturingListeners.clear();
// 移除所有触摸和鼠标事件监听
if (this._touchListener || this._mouseListener) {
eventManager.removeListeners(this);
if (this._touchListener) {
this._touchListener.owner = null;
this._touchListener.mask = null;
this._touchListener = null;
}
if (this._mouseListener) {
this._mouseListener.owner = null;
this._mouseListener.mask = null;
this._mouseListener = null;
}
}
if (CC_JSB && CC_NATIVERENDERER) {
this._proxy.destroy();
this._proxy = null;
}
// 回收到对象池中
this._backDataIntoPool();
if (this._reorderChildDirty) {
cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
}
if (!destroyByParent) {
if (CC_EDITOR) {
// 确保编辑模式下的,节点的被删除后可以通过ctrl+z撤销(重新添加到原来的父节点)
this._parent = null;
}
}
},
// BaseNode的_onPreDestroy
_onPreDestroy () {
var i, len;
// 加上Destroying标记
this._objFlags |= Destroying;
var parent = this._parent;
// 根据检测父节点的标记判断是不是由父节点的destroy发起的释放
var destroyByParent = parent && (parent._objFlags & Destroying);
if (!destroyByParent && (CC_EDITOR || CC_TEST)) {
// 从编辑器中移除
this._registerIfAttached(false);
}
// 把所有子节点进行释放,它们的_onPreDestroy也会被执行
var children = this._children;
for (i = 0, len = children.length; i < len; ++i) {
children[i]._destroyImmediate();
}
// 把所有的组件进行释放,它们的_onPreDestroy也会被执行
for (i = 0, len = this._components.length; i < len; ++i) {
var component = this._components[i];
component._destroyImmediate();
}
// 注销事件监听,比如otherNode.on(type, callback, thisNode) 注册了事件
// thisNode被释放时,需要注销otherNode身上的监听,避免事件回调到已销毁的对象上
var eventTargets = this.__eventTargets;
for (i = 0, len = eventTargets.length; i < len; ++i) {
var target = eventTargets[i];
target && target.targetOff(this);
}
eventTargets.length = 0;
// 如果自己是常驻节点,则从常驻节点列表中移除
if (this._persistNode) {
cc.game.removePersistRootNode(this);
}
// 如果是自己释放的自己,而不是从父节点释放的,要通知父节点,把这个失效的子节点移除掉
if (!destroyByParent) {
if (parent) {
var childIndex = parent._children.indexOf(this);
parent._children.splice(childIndex, 1);
parent.emit && parent.emit('child-removed', this);
}
}
return destroyByParent;
},
// Component的_onPreDestroy
_onPreDestroy () {
// 移除ActionManagerExist和schedule
if (ActionManagerExist) {
cc.director.getActionManager().removeAllActionsFromTarget(this);
}
this.unscheduleAllCallbacks();
// 移除所有的监听
var eventTargets = this.__eventTargets;
for (var i = eventTargets.length - 1; i >= 0; --i) {
var target = eventTargets[i];
target && target.targetOff(this);
}
eventTargets.length = 0;
// 编辑器模式下停止监控
if (CC_EDITOR && !CC_TEST) {
_Scene.AssetsWatcher.stop(this);
}
// destroyComp的实现为调用组件的onDestroy回调,各个组件会在回调中销毁自身的资源
// 比如RigidBody3D组件会调用body的destroy方法,而Animation组件会调用stop方法
cc.director._nodeActivator.destroyComp(this);
// 将组件从节点身上移除
this.node._removeComponent(this);
},
资源释放的问题
最后我们来聊一聊资源释放的问题与定位,在加入引用计数后,最常见的问题还是没有正确增减引用计数导致的内存泄露(循环引用、少调用了decRef或多调用了addRef),以及正在使用的资源被释放的问题(和内存泄露相反,资源被提前释放了)。
从目前的代码来看,如果正确使用了引用计数,新的资源底层是可以避免内存泄露等问题的
这种问题怎么解决呢?首先是定位出哪些资源出了问题,如果是被提前释放,我们可以直接定位到这个资源,如果是内存泄露,当我们发现问题时程序往往已经占用了大量的内存,这种情况下可以切换到一个空场景,并清理资源,把资源清理完后,可以检查assets中残留的资源是否有未被释放的资源。
要了解资源为什么会泄露,可以通过跟踪addRef和decRef的调用得到,下面提供了一个示例方法,用于跟踪某资源的addRef和decRef调用,然后调用资源的dump方法打印出所有调用的堆栈:
public static traceObject(obj : cc.Asset) {
let addRefFunc = obj.addRef;
let decRefFunc = obj.decRef;
let traceMap = new Map();
obj.addRef = function() : cc.Asset {
let stack = ResUtil.getCallStack(1);
let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1;
traceMap.set(stack, cnt);
return addRefFunc.apply(obj, arguments);
}
obj.decRef = function() : cc.Asset {
let stack = ResUtil.getCallStack(1);
let cnt = traceMap.has(stack) ? traceMap.get(stack) + 1 : 1;
traceMap.set(stack, cnt);
return decRefFunc.apply(obj, arguments);
}
obj['dump'] = function() {
console.log(traceMap);
}
}