接着《Cocos Creator 通用框架设计 —— 资源管理》聊聊资源管理框架后续的一些优化:

通过论坛和github的issue,收到了很多优化或bug的反馈,基本上抽空全部处理了,大概做了这么一些事情。

https://github.com/wyb10a10/cocos_creator_framework

  • 修复重复引用泄露bug
  • 修复md5构建泄露bug
  • 修复龙骨动画依赖资源释放bug
  • 修复微信下的依赖构建bug
  • 修复持久节点释放bug
  • 优化了资源依赖结构
  • 支持了资源目录和数组的批量加载和释放
  • 支持了远程资源的管理
  • 新增场景资源的管理
  • 新增ResKeeper统一自动化管理资源
  • 新增内存泄露检测工具
  • 新增资源池和对象池

这篇文章简单分享一下几个重要的优化点

资源依赖结构优化

在资源管理框架中,每个资源都由2部分引用组成,ref和use,ref表示依赖引用,use表示资源的使用,这个优化主要针对ref引用的,当出现下面这样的资源依赖时,B、C、D的ref中会插入A,而E中会插入A和D,这是当时为了fix一些泄露bug的愚蠢实现,正确的实现中E的ref应该只有D,这样既节省空间又节省时间,当一个资源不应该被释放时,没有必要去遍历它的依赖项。在构建依赖项时,如果这个资源的依赖树已经建立,也可以直接使用,不需要再去遍历。

当我们loadRes(A),然后再loadRes(D),此时如果releaseRes(A),D和E是不应该被释放的。这里通过在loadRes时添加自身的ref来控制。

场景资源的管理

当时《Cocos Creator 通用框架设计 —— 资源管理》发布时,并没有处理场景自动加载的资源,这也导致了一些bug,比如误释放场景的资源后会导致报错。

因为ResLoader可以管理它加载的所有资源,而场景的资源是Cocos引擎底层加载的,所以不在ResLoader的管理范围之内,如果说使用ResLoader需要去修改引擎的话,其实不是很友好,当时在论坛中有很多讨论,最终在不修改引擎的前提下实现了一个相对优雅的方案,可以正确的管理场景资源,而且ResLoader的使用者不需要做任何额外处理,对于使用者而言是完全透明的。

一开始的思路是简单判断要释放的资源是不是场景资源,是则直接跳过释放,这样就不会误删场景资源了,但这个操作不方便判断预加载场景的资源,也不能把该释放的资源清干净,所以重新梳理整个场景切换流程后(详情查看这里),有了一个新思路。

在ResLoader初始化时以及场景切换时,对场景资源进行缓存,由于引擎对场景资源有一个自动释放的处理,所以纳入ResLoader管理的场景资源,并不能完全由ResLoader释放,一个场景依赖的资源可能有以下3种:自动释放资源、不自动释放资源、常驻节点依赖的资源。

对待这些资源我们需要区别处理,自动释放资源由于场景切换流程会自动释放它,所以我们只需要简单地移除ResLoader对它的引用即可,无需去释放它。可能有人会问,如果一个自动释放的资源,我在ResLoader中用loadRes加载依赖了它,场景切换时把它自动释放了怎么办?并不会有问题,当我们去load一个资源时,它和它依赖的资源会自动从场景的autoRelease表中移除。

常驻节点的资源不能被自动释放,因为它们在下一个场景中也需要使用。剩下的就是不自动释放的资源,我们需要去释放它。

对于不自动释放的资源,可能是没有勾选autoRelease,也可能是我们在游戏中load了它,我们希望这种资源在场景切换的时候ResLoader能够根据资源实际的引用情况自动释放,如果我有一些公共资源就是不想释放,想留到下一个场景用怎么办?可以用ResLoader进行引用。

    public constructor() {
// 1. 构造当前场景依赖
let scene = cc.director.getScene();
if (scene) {
this._cacheScene(scene);
}
// 2. 监听场景切换
cc.director.on(cc.Director.EVENT_BEFORE_SCENE_LAUNCH, (scene) => {
this._cacheScene(scene);
});
} /**
* 缓存场景
* @param scene
*/
private _cacheScene(scene: cc.Scene) {
// 切换的场景名相同,无需清理资源
if (scene.name == this._lastScene) {
return;
} let refKey = ccloader._getReferenceKey(scene.uuid);
let item = ccloader._cache[refKey];
let newUseKey = `@Scene${this.nextUseKey()}`;
let depends: string[] = null;
if (item) {
depends = this._cacheSceneDepend(item.dependKeys, newUseKey);
} else if(scene["dependAssets"]) {
depends = this._cacheSceneDepend(scene["dependAssets"], newUseKey);
} else {
console.error(`cache scene faile ${scene}`);
return;
}
this._releaseSceneDepend();
this._lastScene = scene.name;
ResLoader._sceneUseKey = newUseKey;
this._sceneDepends = depends;
} /**
* 获得持久节点列表
*/
private _getPersistNodeList() {
let game:any = cc.game;
var persistNodeList = Object.keys(game._persistRootNodes).map(function (x) {
return game._persistRootNodes[x];
});
return persistNodeList;
} private _releaseSceneDepend() {
if (this._sceneDepends) {
let persistDepends : Set<string> = ResUtil.getNodesDepends(this._getPersistNodeList());
for (let i = 0; i < this._sceneDepends.length; ++i) {
// 判断是不是已经被场景切换自动释放的资源,是则直接移除缓存Item(失效项)
let item = this._getResItem(this._sceneDepends[i], undefined);
if (!item) {
this._resMap.delete(this._sceneDepends[i]);
cc.log(`delete untrack res ${this._sceneDepends[i]}`);
}
// 判断是不是持久节点依赖的资源
else if (!persistDepends.has(this._sceneDepends[i])) {
this.releaseRes(this._sceneDepends[i], ResLoader._sceneUseKey);
}
}
this._sceneDepends = null;
}
} private _cacheSceneDepend(depends :string[], useKey: string): string[] {
for (let i = 0; i < depends.length; ++i) {
let item = ccloader._cache[depends[i]];
this._cacheItem(item, useKey);
}
return depends;
}

上面的代码非常简单,在ResLoader的构造函数中我们对当前的场景资源进行了缓存,然后监听cc.Director.EVENT_BEFORE_SCENE_LAUNCH事件,当事件触发的时候,我们缓存新场景的资源,并释放旧场景的资源。

ResKeeper统一自动化管理资源

ResKeeper是一个用于自动化释放资源的组件,如果我们希望很好地控制资源,那么就需要用到use参数,我们是通过use参数来区别各个地方对同一个资源的加载和释放。

ResLoader只是提供了一套简单的机制来保证在使用正确的情况下,能够管理好资源,但直接使用ResLoader确实挺烦人的,因为我们需要手动地去loadRes和releaseRes,如果没有成对地操作,就可能导致资源泄露,更烦人的是,我们还要在加载的时候传不同的use参数,并且在释放的时候把use参数也传进去,从ResLoader本身的角度是合理的,但这样的接口就很不友好了,所以我实现了一个简单的ResKeeper来解决这种烦人的问题。

ResKeeper可以让我们忘掉use参数和releaseRes,只关心要加载什么资源。该组件会自动生成use参数,并记录起来,在节点销毁的时候自动释放资源。

我们可以在任何Node上挂载ResKeeper组件,最合适的挂载点就是UI Node,在接下来的UI框架设计中,我会为每个UIView自动挂载这个组件。在这个UI中我们加载的任何资源都可以在该UI的ResKeeper组件中进行管理,在UI销毁的时候释放这些资源。另外举一个例子比如每个角色、敌人身上可能会加载各种资源,如果我们希望在角色身上自动管理这些资源,也可以在角色身上挂载ResKeeper,在角色销毁的时候自动释放这些资源。

大多数情况下,资源的释放总是跟随在某个节点的销毁之后,一个场景、一个UI、一个层或者一个角色,最新的Cocos Creator中场景无法挂载组件,所以如果想要让资源伴随着场景切换而自动释放,我们需要一点特殊处理(这个处理稍后加上)

@ccclass
export default class ResKeeper extends cc.Component { private autoRes: autoResInfo[] = [];
/**
* 加载资源,通过此接口加载的资源会在界面被销毁时自动释放
* 如果同时有其他地方引用的资源,会解除当前界面对该资源的占用
* @param url 要加载的url
* @param type 类型,如cc.Prefab,cc.SpriteFrame,cc.Texture2D
* @param onCompleted
*/
public loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback) {
let use = resLoader.nextUseKey();
resLoader.loadRes(url, type, (error: Error, res) => {
if (!error) {
this.autoRes.push({ url, use, type });
}
onCompleted && onCompleted(error, res);
}, use);
} /**
* 组件销毁时自动释放所有keep的资源
*/
public onDestroy() {
this.releaseAutoRes();
} /**
* 释放资源,组件销毁时自动调用
*/
public releaseAutoRes() {
for (let index = 0; index < this.autoRes.length; index++) {
const element = this.autoRes[index];
resLoader.releaseRes(element.url, element.type, element.use);
}
this.autoRes.length = 0;
} /**
* 加入一个自动释放的资源
* @param resConf 资源url和类型 [ useKey ]
*/
public autoReleaseRes(resConf: autoResInfo) {
if(resLoader.addUse(resConf.url, resConf.use)) {
this.autoRes.push(resConf);
}
}
}

为了方便对ResLoader的使用,我提供了一个ResUtil,它支持自动获取ResKeeper,比如我们当前的逻辑在UI下的某个节点上的逻辑组件中,我可以通过ResUtil.getResKeeper(this).loadRes(xxx)来加载资源,getResKeeper会向上查找到最近的一个ResKeeper,然后返回给我们。

更方便的接口应该是ResUtil.loadRes,好吧,这个稍后处理!但看到这里你应该能够理解这种自动化管理资源的思路了。我们只需要调用这一个接口来加载资源,资源就会在合适的时候自动释放,现在看起来舒服多了。

export class ResUtil {
/**
* 从目标节点或其父节点递归查找一个资源挂载组件
* @param attachNode 目标节点
* @param autoCreate 当目标节点找不到ResKeeper时是否自动创建一个
*/
public static getResKeeper(attachNode: cc.Node, autoCreate?: boolean): ResKeeper {
if (attachNode) {
let ret = attachNode.getComponent(ResKeeper);
if (!ret) {
if (autoCreate) {
return attachNode.addComponent(ResKeeper);
} else {
return ResUtil.getResKeeper(attachNode.parent, autoCreate);
}
}
return ret;
}
console.error(`can't get ResKeeper for ${attachNode}`);
return null;
} /**
* 赋值srcAsset,并使其跟随targetNode自动释放,用法如下
* mySprite.spriteFrame = AssignWith(otherSpriteFrame, mySpriteNode);
* @param srcAsset 用于赋值的资源,如cc.SpriteFrame、cc.Texture等等
* @param targetNode
* @param autoCreate
*/
public static assignWith(srcAsset: cc.Asset, targetNode: cc.Node, autoCreate?: boolean): any {
let keeper = ResUtil.getResKeeper(targetNode, autoCreate);
if (keeper && srcAsset) {
let url = resLoader.getUrlByAsset(srcAsset);
if (url) {
keeper.autoReleaseRes({ url, use: resLoader.nextUseKey() });
return srcAsset;
}
}
console.error(`AssignWith ${srcAsset} to ${targetNode} faile`);
return null;
} /**
* 实例化一个prefab,并带自动释放功能
* @param prefab 要实例化的预制
*/
public static instantiate(prefab: cc.Prefab): cc.Node {
let node = cc.instantiate(prefab);
let keeper = ResUtil.getResKeeper(node, true);
if (keeper) {
let url = resLoader.getUrlByAsset(prefab);
if (url) {
keeper.autoReleaseRes({ url, type: cc.Prefab, use: resLoader.nextUseKey() });
return node;
}
}
console.warn(`instantiate ${prefab}, autoRelease faile`);
return node;
}
}

基于ResKeeper,我们还可以解决另外一种问题,就是spriteFrame1 = spriteFrame2 这种赋值无法被跟踪的问题,原先,当我们加载了一个资源,如果希望在其他地方使用这个资源,那么需要在另外一个地方进行loadRes,而我们想要的操作可能是直接将这个资源赋值给它,但这样ResLoader无法知道被赋值的资源,所以会出现我们还在使用的资源却被ResLoader释放了的问题。

ResUtil.assignWith可以很好地解决这种问题,它不加载资源,只是简单地在ResLoader中进行注册登记,然后将该资源丢给ResKeeper进行管理。

整体的思路比较清晰,但懒惰的我迟迟没有将接口进行优化完善,让它变得更好用

内存泄露检测工具

这是一个用来辅助检查内存泄露的小工具,使用起来非常简单,可以在项目的ResExample场景中找到它的使用方法。

当我们要开始检查内存泄露的时候,需要把它绑定到ResLoader中,并调用startCheck开始记录接下来的所有资源加载,在合适的时候调用resLoader.resLeakChecker.dump();可以检查当前内存中未释放的资源,以及这些资源时在哪里加载的。

举一个使用的例子,比如我希望检测战斗场景的资源泄露情况,可以在开始加载战斗场景前startCheck,在战斗结束后,将游戏跳转到一个什么都没有的空场景,然后dump查看是否有未释放的资源?

    start() {
let checker = new ResLeakChecker();
checker.startCheck();
resLoader.resLeakChecker = checker;
}

除了startCheck、stopCheck、dump等简单接口外,有时候我们希望过滤一些公共资源的检测,因为我们本来就希望它常驻内存不要释放,ResLeakChecker支持设置一个FilterCallback回调,来帮我们过滤这些资源。

export type FilterCallback = (url: string) => boolean;

export class ResLeakChecker {
public resFilter: FilterCallback = null; public checkFilter(url: string): boolean {
if (!this._checking) {
return false;
}
if (this.resFilter) {
return this.resFilter(url);
}
return true;
}
}

前段时间研究了UE4的实时同步,非常棒的设计以及极其臃肿的实现,6月份会实现一个精简版的网络同步模型,包括用写单机游戏的方式写网络同步游戏,单机模式调通后可以将部分代码部署到nodeJs服务端,实现网络版本,属性同步和RPC支持,这个框架可以快速开发简单的MMO、FPS、ARPG等类型的多人游戏,欢迎star https://github.com/wyb10a10/cocos_creator_framework。

Cocos Creator 通用框架设计 —— 资源管理优化的更多相关文章

  1. Cocos Creator 通用框架设计 —— 资源管理

    如果你想使用Cocos Creator制作一些规模稍大的游戏,那么资源管理是必须解决的问题,随着游戏的进行,你可能会发现游戏的内存占用只升不降,哪怕你当前只用到了极少的资源,并且有使用cc.loade ...

  2. Cocos Creator 通用框架设计 —— 网络

    在Creator中发起一个http请求是比较简单的,但很多游戏希望能够和服务器之间保持长连接,以便服务端能够主动向客户端推送消息,而非总是由客户端发起请求,对于实时性要求较高的游戏更是如此.这里我们会 ...

  3. Android通用框架设计与完整电商APP开发系列文章

    作者|傅猿猿 责编|Javen205 有福利 有福利 有福利 鸣谢 感谢@傅猿猿 邀请写此系列文章 Android通用框架设计与完整电商APP开发 课程介绍 [导学视频] [课程详细介绍] 以下是部分 ...

  4. 新编辑器Cocos Creator发布:对不起我来晚了!

    1月19日,由Cocos创始人王哲亲手撰写的一篇Cocos Creator新品发布稿件在朋友圈被行业人士疯狂转载,短短数小时阅读量突破五位数.Cocos Creator被誉为“注定将揭开Cocos开发 ...

  5. Cocos Creator 性能优化:DrawCall

    前言 在游戏开发中,DrawCall 作为一个非常重要的性能指标,直接影响游戏的整体性能表现. 无论是 Cocos Creator.Unity.Unreal 还是其他游戏引擎,只要说到游戏性能优化,D ...

  6. Cocos Creator—优化首页打开速度

    Cocos Creator是一个优秀的游戏引擎开发工具,很多地方都针对H5游戏做了专门的优化,这是我比较喜欢Cocos Creator的一点原因. 其中一个优化点是首页的加载速度,开发组为了加快首页的 ...

  7. 赢友网络通用框架V10.0.0(WinuAppSoft) 基础框架设计表

    /* * 版权所有:赢友网络(http://www.winu.net/) * 开发人员:新生帝(JsonLei) * 设计名称:赢友网络通用框架V10.0.0(WinuAppSoft) * 设计时间: ...

  8. 痞子衡嵌入式:嵌入式里通用微秒(microseconds)计时函数框架设计与实现

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是嵌入式里通用微秒(microseconds)计时函数框架设计与实现. 在嵌入式软件开发里,计时可以说是非常基础的功能模块了,其应用也非常 ...

  9. Cocos Creator 资源加载流程剖析【一】——cc.loader与加载管线

    这系列文章会对Cocos Creator的资源加载和管理进行深入的剖析.主要包含以下内容: cc.loader与加载管线 Download部分 Load部分 额外流程(MD5 Pipe) 从编辑器到运 ...

随机推荐

  1. 使用 kind 快速搭建 kubernetes 环境

    使用 kind 快速搭建 Kubernetes 环境 Intro kind(Kubernetes IN Docker) 是一个基于 docker 构建 Kubernetes 集群的工具,非常适合用来在 ...

  2. 内存迟迟下不去,可能你就差一个GC.Collect

    一:背景 1. 讲故事 我们有一家top级的淘品牌店铺,为了后续的加速计算,在程序启动的时候灌入她家的核心数据到内存中,灌入完成后内存高达100G,虽然云上的机器内存有256G,然被这么划掉一半看着还 ...

  3. P1714切蛋糕(不定区间最值)

    题面 今天是小Z的生日,同学们为他带来了一块蛋糕.这块蛋糕是一个长方体,被用不同色彩分成了N个相同的小块,每小块都有对应的幸运值. 小Z作为寿星,自然希望吃到的第一块蛋糕的幸运值总和最大,但小Z最多又 ...

  4. Codeforces Round #563 (Div. 2) A-D

    A. Ehab Fails to Be Thanos 这个A题很简单,就是排个序,然后看前面n个数和后面的n个数是不是相同,相同就输出-1 #include <cstdio> #inclu ...

  5. 使用 if elseif else 指定条件

    nrows = 4; ncols = 6; A = ones(nrows,ncols); 遍历矩阵并为每个元素指定一个新值.对主对角线赋值 2,对相邻对角线赋值 -1,对其他位置赋值 0. for c ...

  6. Spring官网阅读(十一)ApplicationContext详细介绍(上)

    文章目录 ApplicationContext 1.ApplicationContext的继承关系 2.ApplicationContext的功能 Spring中的国际化(MessageSource) ...

  7. c#一些常用知识点

    UID自动生成随机数 UID.Text = Guid.NewGuid().ToString(); GridView中常用格式化公式 <asp:BoundField DataField=" ...

  8. docker环境中neo4j导入导出

    neo4j 官方文档有说明,使用 neo4j-admin restore / dump 导出和恢复数据库的时候需要停掉数据,否则会报数据库正在使用的错误:command failed: the dat ...

  9. ubuntu上lib-ace库安装编译

    描述下本人电脑情况: 虚拟机版本:VMware-workstation-full-v7.1.4: ACE版本:ACE6.0.0 虚拟机[Linux](http://lib.csdn.net/base/ ...

  10. indexDB解决过的难题

    我第一次使用indexDB是1年前(2018年10月),运用这个黑科技,解决过3个异常棘手的问题(如果不是indexDB 几乎找不到其他解决方案)所以我经常强调,前端一定要学indexDB! 难题一: ...