Cocos资源管理 —— 静态场景资源建立资源引用关系

这篇文章是在Cocos Creator 通用框架设计 —— 资源管理的基础上,实现了静态场景资源的引用关系,所以阅读前请先阅读上面的那篇文章。

首先非常感谢宝爷的无私奉献,他提出的这个资源管理的方案我是非常认同的,之前就一直想要写一个奈何水平不够。

宝爷的实现只针对动态资源做了一个管理,对于场景资源动态组件有过文字上的建议。我们项目用到了,所以遇到的这些问题在解决和初步测试之后,第一时间跟大家分享,有问题及时反馈,共同进步。

关于场景资源的建议:

关于动态组件的建议:

接下来就跟大家分享一下我自己的理解和处理方案。

Github工程地址:点这里

场景的加载和预加载

场景分为加载和预加载两种,在建立资源引用关系的过程中,这两种是要分开对待。

场景加载

如果你用cc.director.loadScene来加载一个场景,那么你最终会得到一个cc.Scene,你可以在loadScene的onLaunched回调函数获取scene,或者是通过cc.director.getScene()获取。获取的结果如下:

其中dependAssets字段就是该场景所依赖的所有资源(直接依赖和间接依赖都在这里)。

场景预加载

首先来看下preloadScene函数

preloadScene(sceneName: string, onProgress?: (completedCount: number, totalCount: number, item: any) => void, onLoaded?: (error: Error, asset: SceneAsset) => void): void;	

可以看到,onLoaded回调函数的第二个参数返回的是cc.SceneAsset。当我们用preloadScene预加载一个场景时,可以得到这样的结果:

cc.Scene只是cc.SceneAsset的一个变量。而在预加载的时候,cc.Scene里面的dependAssets并没有数据。

那么,对于预加载的场景,我们要怎么获取到它的资源引用关系呢?

对于预加载的场景,虽然cc.Scene的dependAssets没有数据,但是cc.SceneAsset并没有被释放掉,我们可以通过cc.SceneAsset的_uuid(这里是2d8df980-044a-4caf-add3-e3464e761aaf)去获取资源Item,通过Item的dependKeys就可以获取直接依赖的资源,这样就可以建立起预加载场景资源的引用关系了。

场景资源的引用关系

建立引用关系的代码实现

    /**
     * 建立场景的资源引用关系
     * @param asset cc.SceneAsset
     */
    public initSceneResDeps(asset?: cc.SceneAsset) {
        let addDependKey = (item, refKey) => {
            if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                for (let depKey of item.dependKeys) {
                    // 记录这个资源被引用
                    this.getCacheInfo(depKey).refs.add(refKey);
                    // console.log('refs', depKey, refKey);
                    let ccloader: any = cc.loader;
                    let depItem = ccloader._cache[depKey];
                    addDependKey(depItem, refKey);
                }
            }
        }

        // 预加载场景传入asset
        if (asset) {
            let uuid = asset['_uuid'];
            SceneUuidMap.set(asset.name, uuid);
            if (this._loadedSceneRecord.indexOf(uuid) != -1) {
                return;
            }
            this._loadedSceneRecord.push(uuid);
            let useKey = ResourceLoader.createUseKey(`Scene_${asset.name}`);
            let ccloader: any = cc.loader;
            let refKey = ccloader._getReferenceKey(uuid);
            // 通过uuid来获取item,这个item会在loadScene之后删除
            let item = ccloader._cache[refKey];

            if (asset.scene.autoReleaseAssets) {
                console.error(`当前场景${asset.name}不能设置为自动释放资源`);
            }
            console.log(`为预加载场景${asset.name}建立其所依赖的资源引用关系`);

            // 给所有依赖的资源添加ref
            addDependKey(item, uuid);

            // 给直接依赖的资源添加use
            for (let i = 0; i < item.dependKeys.length; i++) {
                let depItem = this._getResItem(item.dependKeys[i]);
                this.getCacheInfo(depItem.id).uses.add(useKey);
            }
        } else {
            // 这里为什么不用uuid去获取item呢,因为在cc.AssetLibrary.loadAsset方法,加载完场景之后会将该item移除,不知道为何
            // 所以这里获取dependAssets,dependAssets记录着场景直接和间接引用的所有资源
            let scene = cc.director.getScene();
            let dependAssets: Array<string> = scene['dependAssets'];
            SceneUuidMap.set(scene.name, scene.uuid);
            if (this._loadedSceneRecord.indexOf(scene.uuid) != -1) {
                return;
            }
            this._loadedSceneRecord.push(scene.uuid);

            if (scene.autoReleaseAssets) {
                console.error(`当前场景${scene.name}不能设置为自动释放资源`);
            }
            console.log(`为场景${scene.name}建立其所依赖的资源引用关系`);

            // 直接依赖的refs添加scene.uuid,uses添加场景useKey
            // 非直接依赖的refs添加scene.uuid
            let useKey = ResourceLoader.createUseKey(`Scene_${scene.name}`);
            for (let i = 0; i < dependAssets.length; i++) {
                let item = this._getResItem(dependAssets[i]);
                let info = this.getCacheInfo(item.id);
                if (!info.refs.has(scene.uuid)) {
                    this.getCacheInfo(item.id).refs.add(scene.uuid);
                    this.getCacheInfo(item.id).uses.add(useKey);
                    addDependKey(item, scene.uuid);
                }
            }
        }
    }

这里我自己封装了场景加载和预加载的工具类SceneUtil,并且在适当的时机调用initSceneResDeps方法。

public static preloadScene(sceneName: string, onProgress?: (completedCount: number, totalCount: number, item: any) => void, onLoaded?: (error: Error, asset: cc.SceneAsset) => void) {
    cc.director.preloadScene(sceneName, onProgress, (error: Error, asset: cc.SceneAsset) => {
        ResourceLoader.getInstance().initSceneResDeps(asset);
        onLoaded && onLoaded(error, asset);
    })
}

public static switchScene(sceneName: string, onLaunched?: Function) {
    SceneUtil.lastSceneName = cc.director.getScene().name;
    cc.director.loadScene(sceneName, (err, scene: cc.Scene) => {
        onLaunched && onLaunched(err, scene);
        ResourceLoader.getInstance().initSceneResDeps();
        // 必须先建立新场景的资源关系,再释放上一个场景
        ResourceLoader.getInstance().releaseSceneResDeps(SceneUtil.lastSceneName);
    });
}

建立结果

一个场景A:

场景A + 场景B:

释放引用关系的代码实现

   /**
     * 释放指定场景引用的资源
     * @param sceneName 
     */
    public releaseSceneResDeps(sceneName: string) {
        console.log(`释放场景${sceneName}所依赖的资源`);
        let useKey = ResourceLoader.createUseKey(`Scene_${sceneName}`);
        let uuid = SceneUuidMap.get(sceneName);
        let index = this._loadedSceneRecord.indexOf(uuid);
        this._loadedSceneRecord.splice(index, 1);

        let release = (item, refKey) => {
            let cacheInfo = this.getCacheInfo(item.id);
            cacheInfo.refs.delete(refKey);
            let delDependKey = (item, refKey) => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        let ccloader: any = cc.loader;
                        let depItem = ccloader._cache[depKey];
                        if (depItem)
                            release(depItem, refKey);
                    }
                }
            };
            delDependKey(item, refKey);
            if (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0) {
                // console.log('release item ', item);
                // 如果没有uuid,就直接释放url
                if (item.uuid) {
                    cc.loader.release(item.uuid);
                    // cc.log('resource leader relase item by uuid :' + item.uuid);
                } else {
                    cc.loader.release(item.id);
                    // cc.log('resource leader relase item by id :' + item.id);
                }
                this._resMap.delete(item.id);
            }
        }

        this._resMap.forEach((value, key) => {
            // 找到场景使用到的资源,将其释放,同时会递归释放其依赖的资源
            if (value.uses.has(useKey)) {
                let item = this._getResItem(key);
                value.uses.delete(useKey);
                release(item, uuid);
            }
        });
    }

测试切换场景

这里提供了一个测试小工具

右上角输入框输入res就会打印出所有的资源引用关系,res空格+查询内容支持模糊查询。

右上角输入框输入item就会打印出cc.loader[’_cache’]的内容,同样也是item空格+查询内容支持模糊查询。

场景A切换到场景B

查看资源引用关系

切换场景B后查看资源引用关系



Github工程地址:点这里

24赞

mark!!!

没人看么,只能自己顶了

赞!!感谢分享

1赞

mark

mark

宝爷有空帮我看下有没有问题,谢了

mark

如果预加载还没完成, 你进出其它场景呢?楼主可以试试吗?

感谢分享

1024!

mark

如果用到预加载的话,一定要自己控制好流程,否则很容易出问题的

mark

mark,也需要用到这块,感谢分享

牛逼。顺便问一个问题。这个最终加载的时候不是调的cc.loader.load么

creator加载只能用cc.loader,所以我们在这个基础上维护引用关系,自己管理

mark

像这种人不仅要点一个赞,还要带上深深的赞意评论一下

1赞

楼主,我这里有一个问题想要请教你一下,场景的静态资源勾选场景的静态释放后,资源的依赖会跟着释放吗?如果不,那岂不是场景的自动释放,释放的不干净?有没有好的解决办法?