想做更深入的加載優(yōu)化?剖析Cocos引擎底層架構(gòu)后,樂府大佬交出「90分答案」
引言:無論是對引擎研發(fā)團(tuán)隊(duì)或是游戲開發(fā)團(tuán)隊(duì)來說,優(yōu)化的重要性都不言而喻。本次,來自樂府互娛的「樂府小學(xué)生」在實(shí)際項(xiàng)目開發(fā)中,通過修改引擎源碼實(shí)現(xiàn)了更加深入的加載優(yōu)化。
游戲江湖上曾流傳過一句名言:“三流的游戲做功能,二流的游戲做表現(xiàn),一流的游戲做優(yōu)化。”雖然有點(diǎn)扯,但并非全無道理,至少說明了優(yōu)化在做游戲中的重要性。本文將結(jié)合我參與的項(xiàng)目實(shí)例,分享我們是如何“站在 Cocos Creator 的肩膀上”做更深入的加載優(yōu)化。
本文所用引擎版本為 Cocos Creator 2.4.6。
一、原音重現(xiàn)
Cocos Creator 的加載流程

以上是 loadRes 的加載流程,其中的關(guān)鍵步驟說明如下:
url tranform:主要是將工程路徑地址 /uuid 轉(zhuǎn)換成對應(yīng)的實(shí)際資源地址。
load res:主要是文件的 IO 過程,并把加載后的資源轉(zhuǎn)成對應(yīng)的 Json 對象或二進(jìn)制數(shù)組。
parse:主要是把加載到的資源解析成對應(yīng)的對象。
depends:獲取當(dāng)前資源的依賴,然后繼續(xù)調(diào)用開始的步驟加載。
剖析 Prefab 的加載流

以上流程左側(cè)清晰地展示了 Cocos Creator 的加載管線,從引擎源碼獲知從 url transform 至 depends 前的流程都可以插入自定義管線,具備較好的靈活性和擴(kuò)展性。
右側(cè)部分為 cc.Spriteframe 資源的加載流程,這里為了展示區(qū)別,我們將其與 Cocos2d-x 中的 CCSprite 加載進(jìn)行對比:

不難看出在 Cocos Creator 中創(chuàng)建一個 Sprite 會比 Cocos2d-x 時多兩個流程。而從 IO 次數(shù)上對比,單張貼圖的加載上 Cocos Creator 比 Cocos2d-x 多2次 IO(SpriteFrame 配置和 Texture2d 配置)。那么這兩個配置是否是必要的?
答案還得從 Cocos Creator 本身的特性說起:
1、SpriteFrame 配置文件(下文簡稱【配置1】):一個獨(dú)立的 json 文件,用來存儲一九宮,以及紋理大小偏移等信息??梢允辜y理自定義修改九宮圖等更靈活。對應(yīng)的就是下面屬性面板中的信息:

TIPS :Cocos2d-x 時期的配置是保存在對應(yīng) ui 編輯器生成的配置文件里,其他沒有被界面引用的資源,需要在代碼中指定配置。
2、Texture2d 的配置(下文簡稱【配置2】):主要定義紋理相關(guān)屬性。

上圖顯示,有兩個屬性配置(WarpMode, FilterMode)會使我們使用圖片和修改配置上更靈活。
綜上,Cocos Creator 加載流程多出的兩個配置是必要的。那么在效率上是否有優(yōu)化空間?
二、選 A 還是選 C
官方的構(gòu)建發(fā)布界面上有關(guān)于貼圖配置的合并選項(xiàng):

官方文檔的解釋如下:
內(nèi)聯(lián)所有 SpriteFrame
自動合并資源時,將所有 SpriteFrame 與被依賴的資源合并到同一個包中。建議網(wǎng)頁平臺開啟,啟用后會略微增大總包體,多消耗一點(diǎn)點(diǎn)網(wǎng)絡(luò)流量,但是能顯著減少網(wǎng)絡(luò)請求數(shù)量。建議原生平臺關(guān)閉,因?yàn)闀龃鬅岣聲r的體積。
合并圖集中的 SpriteFrame
將圖集中的全部 SpriteFrame 合并到同一個包中。默認(rèn)關(guān)閉,啟用后能夠減少熱更新時需要下載的 SpriteFrame 文件數(shù)量,但如果圖集中的 SpriteFrame 數(shù)量很多,則可能會稍微延長原生平臺上的啟動時間。
如果項(xiàng)目中圖集較多,有可能會導(dǎo)致 project.manifest 文件過大,建議勾選該項(xiàng)來減小 project.manifest 的體積。
注意:在熱更新時,需要確保新舊項(xiàng)目中該功能的開啟/關(guān)閉狀態(tài)保持一致,否則會導(dǎo)致熱更新之后出現(xiàn)資源引用錯誤的情況。
通俗的解釋就是:
內(nèi)聯(lián):將 SpriteFrame 對應(yīng)的 json 文件【配置1】合并到了 prefab 中。
合并圖集:把自動圖集中所有 SpriteFrame 合并到同一個文件中,類似 TexturePacker 的 plist 文件。
各自的優(yōu)缺點(diǎn),在官方文檔中有詳細(xì)描述。那么有沒有一種解決方案,即能提高加載效率,又不影響啟動速度呢?
三、90分答案
本項(xiàng)目所采用的解決辦法是:
合并所有的 SpriteFrame 的配置,減少 IO。
將合并后的配置轉(zhuǎn)成二進(jìn)制文件,加快啟動速度。
SpriteFrame 配置優(yōu)化
下面是 SpriteFrame 配置信息,只有 "e8Ueib+qJEhL6mXAHdnwbi"(依賴)和中間的數(shù)據(jù)區(qū)是不同的:
[
??1,
??[
????"e8Ueib+qJEhL6mXAHdnwbi"
??],
??[
????"_textureSetter"
??],
??[
????"cc.SpriteFrame"
??],
??0,
??[
????{
??????"name":?"default_btn_normal",
??????"rect":?[
????????0,
????????0,
????????40,
????????40
??????],
??????"offset":?[
????????0,
????????0
??????],
??????"originalSize":?[
????????40,
????????40
??????],
??????"capInsets":?[
????????12,
????????12,
????????12,
????????12
??????]
????}
??],
??[
????0
??],
??0,
??[
????0
??],
??[
????0
??],
??[
????0
??]
]
解決方案
1、相同的部分作為模板定義在代碼中(減少冗余數(shù)據(jù)),提取所有的差異部分合并到同一個文件中,組成如下配置:
{[
{
??????"name":?"default_btn_normal",
??????"rect":?[
????????0,
????????0,
????????40,
????????40
??????],
??????"offset":?[
????????0,
????????0
??????],
??????"originalSize":?[
????????40,
????????40
??????],
??????"capInsets":?[
????????12,
????????12,
????????12,
????????12
??????],
??????"depend":?"e8Ueib+qJEhL6mXAHdnwbi"?//?額外加入字段
?},
?...
?],
?[uuid1,uuid2,...]?//?額外加入字段為文件的uuid,與上面的順序保持一致
}
2、將文件轉(zhuǎn)成二進(jìn)制格式,這樣可以有效降低文件大小,提高初始化速度,并且減少數(shù)據(jù)和字段冗余。二進(jìn)制方案推薦使用 flatbuffers,具體使用方法可以參考網(wǎng)上教程或官方文檔。
3、接管游戲下載流程,保證文件正常讀取。
3.1 接管 IO:修改 builtin/jsb-adapter/engine/ jsb-fs-utils.js 文件,添加如下:
setJsonReadHandler(handler)?{
????????fsUtils._customJsonLoadHandler?=?handler
????},
????readJson?(filePath,?onComplete)?{
????????let?jsonLoadhandler?=?fsUtils._customJsonLoadHandler
????????if?(jsonLoadhandler?&&?jsonLoadhandler(filePath,?onComplete))?{
????????????return
????????}
????????fsUtils.readFile(filePath,?'utf8',?function?(err,?text)?{
????????????var?out?=?null;
????????????if?(!err)?{
????????????????try?{
????????????????????out?=?JSON.parse(text);
????????????????}
????????????????catch?(e)?{
????????????????????cc.warn(`Read?json?failed:?path:?${filePath}?message:?${e.message}`);
????????????????????err?=?new?Error(e.message);
????????????????}
????????????}
????????????onComplete?&&?onComplete(err,?out);
????????});
????},注:這里是原生端的修改部分,網(wǎng)頁端可以通過自定義加載管線的方式處理
3.2 數(shù)據(jù)還原:通過模板數(shù)據(jù)和二進(jìn)制數(shù)據(jù)對 SpriteFrame 格式做還原,是這里的數(shù)據(jù)區(qū)存為 flatbuffers 對象即可,用到的地方再去解析:
[
??1,
??[
????"e8Ueib+qJEhL6mXAHdnwbi"
??],
??[
????"_textureSetter"
??],
??[
????"cc.SpriteFrame"
??],
??0,
??[
????//?flatbuffer對象
??],
??[
????0
??],
??0,
??[
????0
??],
??[
????0
??],
??[
????0
??]
]
3.3 修改 CCSpriteframe.js 文件,修改解析:
_deserialize:?function?(data,?handle)?{
????????if?(!CC_EDITOR?&&?data.bb)?{
????????????this._deserializeWithFlatbuffers(data);
????????????return;
????????}
????????...
}
Texture2d 配置優(yōu)化
Texture2d 的配置如下:
[
??1,
??0,
??0,
??[
????"cc.Texture2D"
??],
??0,
??[
????"0,9729,9729,33071,33071,0,0,1",
????-1
??],
??[
????0
??],
??0,
??[],
??[],
??[]
]
與 SpriteFrame 配置相比,Texture2d 的配置簡單多了,里面的屬性值主要是與屬性面板和文件擴(kuò)展名有關(guān)。如果圖片的屬性都是默認(rèn)的,并且擴(kuò)展名是相同的情況下,Texture2d 配置是完全相同的,即項(xiàng)目中若有200張圖片資源,那200個圖片的配置文件就是完全相同的。
解決方案
通過 md5 比對所有的 Texture2d 配置文件,提取不同的文件,生成對應(yīng)的配置映射以便快速讀取。以我當(dāng)前的項(xiàng)目為例:有9000+圖片資源,最終比對下來也就只有5種類型,所以就直接把這5種配置在代碼中寫死,同樣在上面的接管流程中返回對應(yīng)的配置信息。
優(yōu)化前后,iphone6 測試的加載速度提升了43%左右:

Texture2d 加載流程優(yōu)化
原生的紋理加載的流程,把紋理數(shù)據(jù)轉(zhuǎn)換成 ArrayBuffer 傳給 js,然后在 js 層再重新組裝返回 C++ 層,這里存在兩次數(shù)據(jù)傳遞的過程。流程如下:

優(yōu)化的方向:在加載完成后,原生層一步到位。直接創(chuàng)建成 Texture2d 對象返回,減少中間的數(shù)據(jù)傳入過程。修改后的流程如下(紅框部分為省略的部分):

注:修改為如上流程后,原生端的動態(tài)合圖將無法使用。但是大多數(shù)的原生開發(fā)都會使用壓縮紋理,并且壓縮紋理也是不支持動態(tài)合圖的。所以動態(tài)合圖的問題大家完全可以忽略。
代碼修改如下:
C++ 部分:
cocos2d-x/cocos/scripting/js-bindings/manual/jsb_global.cpp
...
if?(loadSucceed)
{
??se::Object*?retObj?=?se::Object::createPlainObject();
??retObj->root();
??refs.push_back(retObj);
??cocos2d::renderer::Texture2D*?cobj?=?new?(std::nothrow)?cocos2d::renderer::Texture2D();
??auto?obj?=?se::Object::createObjectWithClass(__jsb_cocos2d_renderer_Texture2D_class);
??obj->setPrivateData(cobj);
??cocos2d::renderer::Texture::Options?options;
??options.bpp?=?imgInfo->bpp;
??options.width?=?imgInfo->width;
??options.height?=?imgInfo->height;
??options.glType?=?imgInfo->type;
??options.glFormat?=?imgInfo->glFormat;
??options.glInternalFormat?=?imgInfo->glInternalFormat;
??options.compressed?=?imgInfo->compressed;
??options.hasMipmap?=?false;
??options.premultiplyAlpha?=?imgInfo->hasPremultipliedAlpha;
??std::vector?images;
??cocos2d::renderer::Texture::Image?image;
??image.data?=?imgInfo->data;
??image.length?=?imgInfo->length;
??images.push_back(image);
??options.images?=?images;
??cobj->initWithOptions(options);
??retObj->setProperty("texture",?se::Value(obj));
??retObj->setProperty("width",?se::Value(imgInfo->width));
??retObj->setProperty("height",?se::Value(imgInfo->height));
??seArgs.push_back(se::Value(retObj));
??imgInfo?=?nullptr;
}
...
JS 代碼修改:
builtin/jsb-adapter/builtin/jsb-adapter/HTMLImageElement.js
set?src(src)?{
?this._src?=?src;
?jsb.loadImage(src,?(info)?=>?{
????if?(!info)?{
????????this._data?=?null;
????????return;
????}?else?if?(info?&&?info.errorMsg)?{
????????this._data?=?null;
????????var?event?=?new?Event('error');
????????this.dispatchEvent(event);
????????return;
????}
????this.width?=?this.naturalWidth?=?info.width;
????this.height?=?this.naturalHeight?=?info.height;
???if?(info.texture)?{
????????info.texture._ctor()
????????this.texture?=?info.texture
????}
????else?{
?????????...
????}
????this.complete?=?true;
????var?event?=?new?Event('load');
????this.dispatchEvent(event);
});
}
engine/cocos2d/core/assets/CCTexture.js
_nativeAsset:?{
????get?()?{
????????//?maybe?returned?to?pool?in?webgl
????????return?this._image;
????},
????set?(data)?{
????????if?(data.texture)?{
????????????this.initWithTexture(data.texture,?data.width,?data.height)
????????????return
????????}
????????...
????}
},
//?添加如下函數(shù)
initWithTexture?(texture,?pixelsWidth,?pixelsHeight)?{
????this._texture?=?texture
????this.width?=?pixelsWidth;
????this.height?=?pixelsHeight;
????//?通知原生端更新配置,如果沒有修改texture屬性的,代碼基本跑不到。
????//?_updateNative標(biāo)志在當(dāng)前對象序列化的時候記錄如果配置中的信息和默認(rèn)值不一致時為true
????if?(this._updateNative)?{
????????var?opts?=?_getSharedOptions();
????????opts.minFilter?=?FilterIndex[this._minFilter];
????????opts.magFilter?=?FilterIndex[this._magFilter];
????????opts.wrapS?=?this._wrapS;
????????opts.wrapT?=?this._wrapT;
????????texture.update(opts,?true)?//?這里需要在原生端添加一個簡易的更新函數(shù)。就拿原來的更新函數(shù)提出紋理數(shù)據(jù)就好了,這里就不貼了。
????}
????this.loaded?=?true;
????this.emit("load");
????return?true;
},
優(yōu)化前后,iphone6 測試的加載速度提升了?12%-15%?左右:

以上統(tǒng)計(jì)的是 Prefab 加載前后的數(shù)據(jù),包含了異步加載紋理的時間,所以會有時間較長的情況,但是同步耗時的地方基本沒了,并且在 iphone6 上已經(jīng)感受不到明顯的卡頓了。
四、附加題
spine 加載優(yōu)化
由于 spine 的骨骼動畫是在原生端單獨(dú)加載的,所以在 js 加載的時候可以移除 spine 骨骼加載,減少一次 IO。
修改文件如下:
deserialize.js
function?deserialize?(json,?options)?{
????...????
????//?不是原生端或者不是骨骼文件,spine原生端不加載骨骼文件
????asset._native?&&?(asset.__nativeDepend__?=?!CC_JSB?||?!(asset?instanceof?sp.SkeletonData));
????pool.put(tdInfo);
????return?asset;
}
路徑搜索(fullPathForFilename)
由于第一次路徑填充的時候,需要從所有的路徑里去查找。從小米5上測試發(fā)現(xiàn)每次路徑檢查需要消耗 2ms 左右。正常我們會有兩個路徑:一個更新路徑,一個是當(dāng)前包路徑。所以小米5上一個文件檢索至少要 4ms+。
解決方案:
自己生成一個路徑映射表。因?yàn)榇虬透碌臅r候文件有哪些都是確定的。這樣就可以使文件查找的速度降到 50μs 以下。
本文主要是想分享一個加載優(yōu)化的思路和方向給大家,感興趣的小伙伴可以點(diǎn)擊文末【閱讀原文】前往論壇專貼一起交流討論:
https://forum.cocos.org/t/topic/134363
往期精彩



