CocosCreator通用框架設(shè)計(jì)之資源管理
如果你想使用Cocos Creator制作一些規(guī)模稍大的游戲,那么資源管理是必須解決的問(wèn)題,隨著游戲的進(jìn)行,你可能會(huì)發(fā)現(xiàn)游戲的內(nèi)存占用只升不降,哪怕你當(dāng)前只用到了極少的資源,并且有使用cc.loader.release來(lái)釋放之前加載的資源,但之前使用過(guò)的大部分資源都會(huì)留在內(nèi)存中!為什么會(huì)這樣呢?
cocos creator 資源管理存在的問(wèn)題
資源管理主要解決3個(gè)問(wèn)題,資源加載,資源查找(使用),資源釋放。這里要討論的主要是資源釋放的問(wèn)題,這個(gè)問(wèn)題看上去非常簡(jiǎn)單,在Cocos2d-x中確實(shí)也很簡(jiǎn)單,但在js中變得復(fù)雜了起來(lái),因?yàn)殡y以跟蹤一個(gè)資源是否可以被釋放。
在Cocos2d-x中我們使用引用計(jì)數(shù),在引用計(jì)數(shù)為0的時(shí)候釋放資源,維護(hù)好引用計(jì)數(shù)即可,而且在Cocos2d-x中我們對(duì)資源的管理是比較分散的,引擎層面只提供如TextureCache、AudioManager之類(lèi)的單例來(lái)管理某種特定的資源,大多數(shù)的資源都需要我們自己去管理,而在cocos creator中,我們的資源統(tǒng)一由cc.loader來(lái)管理,大量使用prefab,prefab與各種資源復(fù)雜的引用關(guān)系增加了資源管理的難度。
資源依賴(lài)
資源A可能依賴(lài)資源B、C、D,而資源D又依賴(lài)資源E,這是非常常見(jiàn)的一種資源依賴(lài)情況,如果我們使用cc.loader.loadRes("A")加載資源A,B~E都會(huì)被加載進(jìn)來(lái),但如果我們調(diào)用cc.loader.release("A")則只有資源A被釋放。

每一個(gè)加載的資源都會(huì)放到cc.loader的_cache中,但cc.loader.release只是將傳入的資源進(jìn)行釋放,而沒(méi)有考慮資源依賴(lài)的情況。
如果對(duì)cc.loader背后的資源加載流程感興趣可以參考: https://www.cnblogs.com/ybgame/p/10576884.html
如果我們希望將依賴(lài)的資源也一起釋放,cocos creator提供了一個(gè)笨拙的方法,cc.loader.getDependsRecursively;,遞歸獲取指定資源依賴(lài)的所有資源,放入一個(gè)數(shù)組并返回,然后在cc.loader.release中傳入該數(shù)組,cc.loader會(huì)遍歷它們,將其逐個(gè)釋放。
這種方式雖然可以將資源釋放,但卻有可能釋放了不應(yīng)該釋放的資源,如果有一個(gè)資源F依賴(lài)D,這時(shí)候就會(huì)導(dǎo)致F資源無(wú)法正常工作。由于cocos creator引擎沒(méi)有維護(hù)好資源的依賴(lài),導(dǎo)致我們?cè)卺尫臘的時(shí)候并不知道還有F依賴(lài)我們。即使沒(méi)有F依賴(lài),我們也不確定是否可以釋放D,比如我們調(diào)用cc.loader加載D,而后又加載了A,此時(shí)D已經(jīng)加載完成,A可以直接使用。但如果釋放A的時(shí)候,將D也釋放了,這就不符合我們的預(yù)期,我們期望的是在我們沒(méi)有顯式地釋放D時(shí),D不應(yīng)該隨著其它資源的釋放而自動(dòng)釋放。
可以簡(jiǎn)單地進(jìn)行測(cè)試,可以打開(kāi)Chrome的開(kāi)發(fā)者模式,在Console面板中進(jìn)行輸入,如果是舊版本的cocos creator可以在cc.textureCache中dump所有的紋理,而新版本移除了textureCache,但我們可以輸入cc.loader._cache來(lái)查看所有的資源。如果資源太多,只關(guān)心數(shù)量,可以輸入Object.keys(cc.loader._cache).length來(lái)查看資源總數(shù),我們可以在資源加載前dump一次,加載后dump一次,釋放后再dump一次,來(lái)對(duì)比cc.loader中的緩存狀態(tài)。當(dāng)然,也可以寫(xiě)一些便捷的方法,如只dump圖片,或者dump與上次dump的差異項(xiàng)。

資源使用
除了資源依賴(lài)的問(wèn)題,我們還需要解決資源使用的問(wèn)題,前者是cc.loader內(nèi)部的資源組織問(wèn)題,后者是應(yīng)用層邏輯的資源使用問(wèn)題,比如我們需要在一個(gè)界面關(guān)閉的時(shí)候釋放某資源,同樣會(huì)面臨一個(gè)該不該釋放的問(wèn)題,比如另外一個(gè)未關(guān)閉的界面是否使用了該資源?如果有其他地方用到了該資源,那么就不應(yīng)該釋放它!
ResLoader
在這里我設(shè)計(jì)了一個(gè)ResLoader,來(lái)解決cc.loader沒(méi)有解決好的問(wèn)題,關(guān)鍵是為每一個(gè)資源創(chuàng)建一個(gè)CacheInfo來(lái)記錄資源的依賴(lài)和使用等信息,以此來(lái)判斷資源是否可以釋放,使用ResLoader.getInstance().loadRes()來(lái)替代cc.loader.loadRes(),ResLoader.getInstance().releaseRes()來(lái)替代cc.loader.releaseRes()。
對(duì)于依賴(lài),在資源加載的時(shí)候ResLoader會(huì)自動(dòng)建立起映射,釋放資源的時(shí)候會(huì)自動(dòng)取消映射,并檢測(cè)取消映射后的資源是否可以釋放,是才走釋放的邏輯。
對(duì)于使用,提供了一個(gè)use參數(shù),通過(guò)該參數(shù)來(lái)區(qū)別是哪里使用了該資源,以及是否有其他地方使用了該資源,當(dāng)一個(gè)資源即沒(méi)有倍其他資源依賴(lài),也沒(méi)有被其它邏輯使用,那么這個(gè)資源就可以被釋放。
/**
* 資源加載類(lèi)
* 1. 加載完成后自動(dòng)記錄引用關(guān)系,根據(jù)DependKeys記錄反向依賴(lài)
* 2. 支持資源使用,如某打開(kāi)的UI使用了A資源,其他地方釋放資源B,資源B引用了資源A,如果沒(méi)有其他引用資源A的資源,會(huì)觸發(fā)資源A的釋放,
* 3. 能夠安全釋放依賴(lài)資源(一個(gè)資源同時(shí)被多個(gè)資源引用,只有當(dāng)其他資源都釋放時(shí),該資源才會(huì)被釋放)
*
* 2018-7-17 by 寶爺
*/
// 資源加載的處理回調(diào)
export type ProcessCallback = (completedCount: number, totalCount: number, item: any) => void;
// 資源加載的完成回調(diào)
export type CompletedCallback = (error: Error, resource: any) => void;
// 引用和使用的結(jié)構(gòu)體
interface CacheInfo {
refs: Set<string>,
uses: Set<string>
}
// LoadRes方法的參數(shù)結(jié)構(gòu)
interface LoadResArgs {
url: string,
type?: typeof cc.Asset,
onCompleted?: CompletedCallback,
onProgess?: ProcessCallback,
use?: string,
}
// ReleaseRes方法的參數(shù)結(jié)構(gòu)
interface ReleaseResArgs {
url: string,
type?: typeof cc.Asset,
use?: string,
}
// 兼容性處理
let isChildClassOf = cc.js["isChildClassOf"]
if (!isChildClassOf) {
isChildClassOf = cc["isChildClassOf"];
}
export default class ResLoader {
private _resMap: Map<string, CacheInfo> = new Map<string, CacheInfo>();
private static _resLoader: ResLoader = null;
public static getInstance(): ResLoader {
if (!this._resLoader) {
this._resLoader = new ResLoader();
}
return this._resLoader;
}
public static destroy(): void {
if (this._resLoader) {
this._resLoader = null;
}
}
private constructor() {
}
/**
* 從cc.loader中獲取一個(gè)資源的item
* @param url 查詢(xún)的url
* @param type 查詢(xún)的資源類(lèi)型
*/
private _getResItem(url: string, type: typeof cc.Asset): any {
let ccloader: any = cc.loader;
let item = ccloader._cache[url];
if (!item) {
let uuid = ccloader._getResUuid(url, type, false);
if (uuid) {
let ref = ccloader._getReferenceKey(uuid);
item = ccloader._cache[ref];
}
}
return item;
}
/**
* loadRes方法的參數(shù)預(yù)處理
*/
private _makeLoadResArgs(): LoadResArgs {
if (arguments.length < 1 || typeof arguments[0] != "string") {
console.error(`_makeLoadResArgs error ${arguments}`);
return null;
}
let ret: LoadResArgs = { url: arguments[0] };
for (let i = 1; i < arguments.length; ++i) {
if (i == 1 && isChildClassOf(arguments[i], cc.RawAsset)) {
// 判斷是不是第一個(gè)參數(shù)type
ret.type = arguments[i];
} else if (i == arguments.length - 1 && typeof arguments[i] == "string") {
// 判斷是不是最后一個(gè)參數(shù)use
ret.use = arguments[i];
} else if (typeof arguments[i] == "function") {
// 其他情況為函數(shù)
if (arguments.length > i + 1 && typeof arguments[i + 1] == "function") {
ret.onProgess = arguments[i];
} else {
ret.onCompleted = arguments[i];
}
}
}
return ret;
}
/**
* releaseRes方法的參數(shù)預(yù)處理
*/
private _makeReleaseResArgs(): ReleaseResArgs {
if (arguments.length < 1 || typeof arguments[0] != "string") {
console.error(`_makeReleaseResArgs error ${arguments}`);
return null;
}
let ret: ReleaseResArgs = { url: arguments[0] };
for (let i = 1; i < arguments.length; ++i) {
if (typeof arguments[i] == "string") {
ret.use = arguments[i];
} else {
ret.type = arguments[i];
}
}
return ret;
}
/**
* 生成一個(gè)資源使用Key
* @param where 在哪里使用,如Scene、UI、Pool
* @param who 使用者,如Login、UIHelp...
* @param why 使用原因,自定義...
*/
public static makeUseKey(where: string, who: string = "none", why: string = ""): string {
return `use_${where}_by_${who}_for_${why}`;
}
/**
* 獲取資源緩存信息
* @param key 要獲取的資源url
*/
public getCacheInfo(key: string): CacheInfo {
if (!this._resMap.has(key)) {
this._resMap.set(key, {
refs: new Set<string>(),
uses: new Set<string>()
});
}
return this._resMap.get(key);
}
/**
* 開(kāi)始加載資源
* @param url 資源url
* @param type 資源類(lèi)型,默認(rèn)為null
* @param onProgess 加載進(jìn)度回調(diào)
* @param onCompleted 加載完成回調(diào)
* @param use 資源使用key,根據(jù)makeUseKey方法生成
*/
public loadRes(url: string, use?: string);
public loadRes(url: string, onCompleted: CompletedCallback, use?: string);
public loadRes(url: string, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
public loadRes(url: string, type: typeof cc.Asset, use?: string);
public loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback, use?: string);
public loadRes(url: string, type: typeof cc.Asset, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
public loadRes() {
let resArgs: LoadResArgs = this._makeLoadResArgs.apply(this, arguments);
console.time("loadRes|"+resArgs.url);
let finishCallback = (error: Error, resource: any) => {
// 反向關(guān)聯(lián)引用(為所有引用到的資源打上本資源引用到的標(biāo)記)
let addDependKey = (item, refKey) => {
if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
for (let depKey of item.dependKeys) {
// 記錄該資源被我引用
this.getCacheInfo(depKey).refs.add(refKey);
// cc.log(`${depKey} ref by ${refKey}`);
let ccloader: any = cc.loader;
let depItem = ccloader._cache[depKey]
addDependKey(depItem, refKey)
}
}
}
let item = this._getResItem(resArgs.url, resArgs.type);
if (item && item.url) {
addDependKey(item, item.url);
} else {
cc.warn(`addDependKey item error1! for ${resArgs.url}`);
}
// 給自己加一個(gè)自身的引用
if (item) {
let info = this.getCacheInfo(item.url);
info.refs.add(item.url);
// 更新資源使用
if (resArgs.use) {
info.uses.add(resArgs.use);
}
}
// 執(zhí)行完成回調(diào)
if (resArgs.onCompleted) {
resArgs.onCompleted(error, resource);
}
console.timeEnd("loadRes|"+resArgs.url);
};
// 預(yù)判是否資源已加載
let res = cc.loader.getRes(resArgs.url, resArgs.type);
if (res) {
finishCallback(null, res);
} else {
cc.loader.loadRes(resArgs.url, resArgs.type, resArgs.onProgess, finishCallback);
}
}
/**
* 釋放資源
* @param url 要釋放的url
* @param type 資源類(lèi)型
* @param use 要解除的資源使用key,根據(jù)makeUseKey方法生成
*/
public releaseRes(url: string, use?: string);
public releaseRes(url: string, type: typeof cc.Asset, use?: string)
public releaseRes() {
/**暫時(shí)不釋放資源 */
// return;
let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
let item = this._getResItem(resArgs.url, resArgs.type);
if (!item) {
console.warn(`releaseRes item is null ${resArgs.url} ${resArgs.type}`);
return;
}
cc.log("resloader release item");
// cc.log(arguments);
let cacheInfo = this.getCacheInfo(item.url);
if (resArgs.use) {
cacheInfo.uses.delete(resArgs.use)
}
this._release(item, item.url);
}
// 釋放一個(gè)資源
private _release(item, itemUrl) {
if (!item) {
return;
}
let cacheInfo = this.getCacheInfo(item.url);
// 解除自身對(duì)自己的引用
cacheInfo.refs.delete(itemUrl);
if (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0) {
// 解除引用
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]
this._release(depItem, refKey);
}
}
}
delDependKey(item, itemUrl);
//如果沒(méi)有uuid,就直接釋放url
if (item.uuid) {
cc.loader.release(item.uuid);
cc.log("resloader release item by uuid :" + item.url);
} else {
cc.loader.release(item.url);
cc.log("resloader release item by url:" + item.url);
}
}
}
/**
* 判斷一個(gè)資源能否被釋放
* @param url 資源url
* @param type 資源類(lèi)型
* @param use 要解除的資源使用key,根據(jù)makeUseKey方法生成
*/
public checkReleaseUse(url: string, use?: string): boolean;
public checkReleaseUse(url: string, type: typeof cc.Asset, use?: string): boolean
public checkReleaseUse() {
let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
let item = this._getResItem(resArgs.url, resArgs.type);
if (!item) {
console.log(`cant release,item is null ${resArgs.url} ${resArgs.type}`);
return true;
}
let cacheInfo = this.getCacheInfo(item.url);
let checkUse = false;
let checkRef = false;
if (resArgs.use && cacheInfo.uses.size > 0) {
if (cacheInfo.uses.size == 1 && cacheInfo.uses.has(resArgs.use)) {
checkUse = true;
} else {
checkUse = false;
}
} else {
checkUse = true;
}
if ((cacheInfo.refs.size == 1 && cacheInfo.refs.has(item.url)) || cacheInfo.refs.size == 0) {
checkRef = true;
} else {
checkRef = false;
}
return checkUse && checkRef;
}
}
使用ResLoader
ResLoader的使用非常簡(jiǎn)單,下面是一個(gè)簡(jiǎn)單的例子,我們可以點(diǎn)擊dump按鈕來(lái)查看當(dāng)前的資源總數(shù),點(diǎn)擊cc.load、cc.release之后分別dump一次,可以發(fā)現(xiàn),開(kāi)始有36個(gè)資源,加載之后有40個(gè)資源,而執(zhí)行釋放之后,還有39個(gè)資源,只釋放了一個(gè)資源。
如果使用ResLoader進(jìn)行測(cè)試,發(fā)現(xiàn)釋放之后只有34個(gè)資源,這是因?yàn)榍懊婕虞d場(chǎng)景的資源也被該測(cè)試資源依賴(lài),所以這些資源也被釋放掉了,只要我們都使用ResLoader來(lái)加載和卸載資源,就不會(huì)出現(xiàn)資源泄露的問(wèn)題。

示例代碼:
@ccclass
export default class NetExample extends cc.Component {
@property(cc.Node)
attachNode: cc.Node = null;
@property(cc.Label)
dumpLabel: cc.Label = null;
onLoadRes() {
cc.loader.loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
if (!error) {
cc.instantiate(prefab).parent = this.attachNode;
}
});
}
onUnloadRes() {
this.attachNode.removeAllChildren(true);
cc.loader.releaseRes("Prefab/HelloWorld");
}
onMyLoadRes() {
ResLoader.getInstance().loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
if (!error) {
cc.instantiate(prefab).parent = this.attachNode;
}
});
}
onMyUnloadRes() {
this.attachNode.removeAllChildren(true);
ResLoader.getInstance().releaseRes("Prefab/HelloWorld");
}
onDump() {
let Loader:any = cc.loader;
this.dumpLabel.string = `當(dāng)前資源總數(shù):${Object.keys(Loader._cache).length}`;
}
}
可以看到上面的例子是先移除節(jié)點(diǎn),再進(jìn)行釋放,這是正確的使用方式,如果我沒(méi)有移除直接釋放呢??因?yàn)獒尫帕思y理,所以cocos creator在接下來(lái)的渲染中會(huì)不斷報(bào)錯(cuò)。
ResLoader只是一個(gè)基礎(chǔ),直接使用ResLoader我們不需要關(guān)心資源的依賴(lài)問(wèn)題,但資源的使用問(wèn)題我們還需要關(guān)心,在實(shí)際的使用中,我們可能希望資源的生命周期是以下幾種情況:
- 跟隨某對(duì)象的生命周期,對(duì)象銷(xiāo)毀時(shí)資源釋放
- 跟隨某界面的生命周期,界面關(guān)閉時(shí)資源釋放
- 跟隨某場(chǎng)景的生命周期,場(chǎng)景切換時(shí)資源釋放
我們可以實(shí)現(xiàn)一個(gè)組件掛在到對(duì)象身上,當(dāng)我們?cè)谠搶?duì)象或該對(duì)象的其它組件中編寫(xiě)邏輯,加載資源時(shí),使用這個(gè)資源管理組件進(jìn)行加載,由該組件來(lái)維護(hù)資源的釋放。界面和場(chǎng)景也類(lèi)似。。
項(xiàng)目代碼位于:https://github.com/wyb10a10/cocos_creator_framework ,打開(kāi)Scene目錄的ResExample場(chǎng)景即可查看。
以上就是CocosCreator通用框架設(shè)計(jì)之資源管理的詳細(xì)內(nèi)容,更多關(guān)于CocosCreator框架設(shè)計(jì)之資源管理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Unity3D實(shí)現(xiàn)攝像機(jī)鏡頭移動(dòng)并限制角度
- 詳解CocosCreator中幾種計(jì)時(shí)器的使用方法
- CocosCreator學(xué)習(xí)之模塊化腳本
- 怎樣在CocosCreator中使用物理引擎關(guān)節(jié)
- 如何在CocosCreator中使用JSZip壓縮
- CocosCreator入門(mén)教程之用TS制作第一個(gè)游戲
- 解讀CocosCreator源碼之引擎啟動(dòng)與主循環(huán)
- 如何在CocosCreator中做一個(gè)List
- 如何在CocosCreator中使用http和WebSocket
- 剖析CocosCreator新資源管理系統(tǒng)
- CocosCreator怎樣使用cc.follow進(jìn)行鏡頭跟隨
相關(guān)文章
微信小程序?qū)崿F(xiàn)搜索功能并跳轉(zhuǎn)搜索結(jié)果頁(yè)面
本文主要介紹了微信小程序?qū)崿F(xiàn)搜索功能并跳轉(zhuǎn)搜索結(jié)果頁(yè)面,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
JS實(shí)現(xiàn)兼容性較好的隨屏滾動(dòng)效果
這篇文章主要介紹了JS實(shí)現(xiàn)兼容性較好的隨屏滾動(dòng)效果,演示了固定位置顯示和隨屏滾動(dòng)兩種效果的實(shí)現(xiàn)方法,涉及css樣式的設(shè)置與結(jié)合時(shí)間函數(shù)遞歸調(diào)用實(shí)現(xiàn)滾屏的技巧,需要的朋友可以參考下2015-11-11
完美實(shí)現(xiàn)仿QQ空間評(píng)論回復(fù)特效
這篇文章主要介紹了完美實(shí)現(xiàn)仿QQ空間評(píng)論回復(fù)特效,非常的實(shí)用,附上實(shí)例代碼給大家,有需要的小伙伴參考下吧。2015-05-05

