接着《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. Android APK 重签名

    对APK 进行在线 加固后,Apk体积一般会变大,而且Apk会无法直接安装,因为缺少了你的签名.是的,你需要对这个Apk进行重签名. 如何重签名 重签名的方法,一般来说,有两种,第一种是用JDK自带的 ...

  2. andorid jar/库源码解析之zxing

    目录:andorid jar/库源码解析 Zxing: 作用: 生成和识别,二维码,条形码. 栗子: 生成二维码,赋值到ImageView上 QRCodeWriter qrCodeWriter = n ...

  3. 面试官没想到一个Volatile,我都能跟他扯半小时

    点赞再看,养成习惯,微信搜索[三太子敖丙]关注这个互联网苟且偷生的工具人. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的 ...

  4. 使用Codemirror打造Markdown编辑器

    前几天突然想给自己的在线编译器加一个Markdown编辑功能,于是花了两三天敲敲打打初步实现了这个功能. 一个Markdown编辑器需要有如下常用功能: 粗体 斜体 中划线 标题 链接 图片 引用 代 ...

  5. print函数的全面认识

    # 输出打印 数字 print(123) a = 100 print(a) # 输出打印 字符串 print('字符串123') print('''锄禾日当午 汗滴禾下土''') # 输出打印 列表 ...

  6. crontab自动启动小任务例子(每一分钟将当前日期打入一个文件)

      crontab -l #查看当前定时任务列表 显示没有,那么我们来安装一下(必须在root用户下) – yum install vixie-cron  – yum install crontabs ...

  7. 深入理解CSS定位

    CSS中有3种定位机制:普通流,浮动和绝对定位.除非专门指定,否则所有框都在普通流中定位.顾名思义,普通流中元素框的位置由HTML元素的位置决定.块级框一个接一个地垂直排列,框之间的垂直距离由框的垂直 ...

  8. SQL语言概况(4.1)

    SQL语言概况(4.1) 目录 SQL语言概况(4.1) 4.1 SQL语言概况 4.1.1 历史及标准简介 4.1.2 SQL语言定义及特点 4.1.3 使用说明 参考资料: 数据库原理及设计(第3 ...

  9. C#黔驴技巧之去重(Distinct)

    前言 关于C#中默认的Distinct方法在什么情况下才能去重,这个就不用我再多讲,针对集合对象去重默认实现将不再满足,于是乎我们需要自定义实现来解决这个问题,接下来我们详细讲解几种常见去重方案,孰好 ...

  10. Docker学习笔记:镜像、容器、数据卷

    核心概念 镜像:一个只读的模板,类似虚拟机的镜像. 容器:可以理解为镜像的一个运行实例.运行时类似于沙箱,多个容器互相独立. 仓库:存放镜像文件的地方. 镜像 命令表格 命令 解释 选项 docker ...