Cocos Creator 新资源管理系统剖析【四:资源释放】

资源释放

这一小节重点介绍在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是强制释放。资源释放的完整流程大致如下图所示:

sPlF8f.png

    // 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);
        }
    }
25赞

666666mark

先mark为敬

前排占座 ~~~~~

喵,待日后再看

多半用不到,但是先插眼

[quote=“111304, post:1, topic:103506”]
traceMap
[/quote] mark mark

mark!!!

牛逼,膜拜大神,虽然看的不是很懂。慢慢多看几遍吧

马克~~~~

宝爷,你把针对新资源管理系统把你之前的资源管理类重构吗,期待你的新资源管理类

nice,这是哪个版本,3.0preview可用吗
let stack = ResUtil.getCallStack(1);
这行代码没有实现呢

https://github.com/wyb10a10/cocos_creator_framework/tree/creator2.4.2
前段时间用3.0跑,好像没什么问题

这个月重构一下,之前其实也重构过了,还是得做一个能自动适配新旧资源底层,然后接口上按新资源底层对齐的方案

mark!!!

厉害了大佬,期待更新资源模块^_^

mark!

mark一下!!!

addRef和decRef,我遇到了一个问题,就是我动态加载了资源,进行addRef,在界面关闭的时候,进行decRef。
但这里会出现一个问题,就是两个界面用了同一个动态资源,这里就会出现问题
关闭界面的时候decRef,这时候引用为0,就进入释放了
这个时候同时打开另一个界面,里面也用到了同样的动态资源,这时候加载出来还是上面被释放的资源,这时候再addRef,但是资源却被释放了,用不了
我这种做法是不是又问题?有没什么办法进行避免吗?或者常规的做法是怎么做?把动态加载的内容都放到start里再加载?
我现在都是延迟关闭,或者延迟加载,来解决这个问题

我也遇到过,一般是延迟或者拆成2份用