Cocos Creator 資源載入流程剖析【六】——場景切換流程
這裡討論場景切換的完整流程,從我們呼叫了loadScene開始切換場景,到場景切換完成背後發生的事情。整個流程可以分為場景載入和場景切換兩部分,另外還簡單討論了場景的預載入。
載入場景的流程
loadScene主要做了3件事,通過_getSceneUuid獲取要載入場景的資訊,對於原生平臺的非啟動場景執行了cc.LoaderLayer.preload(但查詢了所有的程式碼,並沒有發現LoaderLayer的實現,也沒有發現任何對cc.runtime賦值的地方),最後通過_loadSceneByUuid載入場景。
loadScene: function (sceneName, onLaunched, _onUnloaded) { // 同一時間只能有一個場景在載入 if (this._loadingScene) { cc.errorID(1213, sceneName, this._loadingScene); return false; } // 獲取場景的資訊 var info = this._getSceneUuid(sceneName); if (info) { var uuid = info.uuid; // 觸發一個場景開始載入的事件 this.emit(cc.Director.EVENT_BEFORE_SCENE_LOADING, sceneName); // 設定當前正在載入的場景 this._loadingScene = sceneName; // 在原生執行時且該場景並非啟動場景時,可以進行非同步載入。 if (CC_JSB && cc.runtime && uuid !== this._launchSceneUuid) { var self = this; var groupName = cc.path.basename(info.url) + '_' + info.uuid; console.log('==> start preload: ' + groupName); var ensureAsync = false; // 如果cc.LoaderLayer.preload是非同步的,會在preload結束後執行_loadSceneByUuid。否則會在preload結束的下一幀執行_loadSceneByUuid。 cc.LoaderLayer.preload([groupName], function () { console.log('==> end preload: ' + groupName); if (ensureAsync) { self._loadSceneByUuid(uuid, onLaunched, _onUnloaded); } else { setTimeout(function () { self._loadSceneByUuid(uuid, onLaunched, _onUnloaded); }, 0); } }); ensureAsync = true; } else { this._loadSceneByUuid(uuid, onLaunched, _onUnloaded); } return true; } else { cc.errorID(1214, sceneName); return false; } },
Creator2.x版本的loadScene則直接多了,執行_getSceneUuid,觸發EVENT_BEFORE_SCENE_LOADING事件,再呼叫_loadSceneByUuid。
loadScene: function (sceneName, onLaunched, _onUnloaded) { if (this._loadingScene) { cc.errorID(1208, sceneName, this._loadingScene); return false; } var info = this._getSceneUuid(sceneName); if (info) { var uuid = info.uuid; this.emit(cc.Director.EVENT_BEFORE_SCENE_LOADING, sceneName); this._loadingScene = sceneName; this._loadSceneByUuid(uuid, onLaunched, _onUnloaded); return true; } else { cc.errorID(1209, sceneName); return false; } },
_loadSceneByUuid方法也很簡單,呼叫了cc.AssetLibrary.loadAsset載入資源,並指定了資源載入結束後的回撥,也就是執行runSceneImmediate以及使用者傳入的onLaunched回撥。
_loadSceneByUuid方法在Creator2.x和Creator1.x中沒有區別
_loadSceneByUuid: function (uuid, onLaunched, onUnloaded, dontRunScene) { if (CC_EDITOR) { if (typeof onLaunched === 'boolean') { dontRunScene = onLaunched; onLaunched = null; } if (typeof onUnloaded === 'boolean') { dontRunScene = onUnloaded; onUnloaded = null; } } console.time('LoadScene ' + uuid); cc.AssetLibrary.loadAsset(uuid, function (error, sceneAsset) { console.timeEnd('LoadScene ' + uuid); var self = cc.director; self._loadingScene = ''; if (error) { error = 'Failed to load scene: ' + error; cc.error(error); } else { // runSceneImmediate啟動場景 if (sceneAsset instanceof cc.SceneAsset) { var scene = sceneAsset.scene; scene._id = sceneAsset._uuid; scene._name = sceneAsset._name; if (CC_EDITOR) { if (!dontRunScene) { self.runSceneImmediate(scene, onUnloaded, onLaunched); } else { scene._load(); if (onLaunched) { onLaunched(null, scene); } } } else { self.runSceneImmediate(scene, onUnloaded, onLaunched); } return; } else { error = 'The asset ' + uuid + ' is not a scene'; cc.error(error); } } if (onLaunched) { onLaunched(error); } }); },
loadAsset做的事情也非常簡單,就是呼叫Loader.load去做真正的載入,在載入完成之後將場景所依賴的資源設定給asset.scene.dependAssets,用於場景的釋放,另外因為場景並不作為一個可重複使用的資源,所以這裡會將場景從Loader中移除。
loadAsset方法在Creator2.x和Creator1.x中沒有區別
loadAsset: function (uuid, callback, options) {
if (typeof uuid !== 'string') {
return callInNextTick(callback, new Error('[AssetLibrary] uuid must be string'), null);
}
var item = {
uuid: uuid,
type: 'uuid'
};
if (options && options.existingAsset) {
item.existingAsset = options.existingAsset;
}
Loader.load(item, function (error, asset) {
if (error || !asset) {
error = new Error('[AssetLibrary] loading JSON or dependencies failed: ' + (error ? error.message : 'Unknown error'));
} else {
if (asset.constructor === cc.SceneAsset) {
if (CC_EDITOR && !asset.scene) {
Editor.error('Sorry, the scene data of "%s" is corrupted!', uuid);
} else {
var key = cc.loader._getReferenceKey(uuid);
// 這裡其實是遞迴獲取場景這個item的dependKeys陣列(去重複)
asset.scene.dependAssets = AutoReleaseUtils.getDependsRecursively(key);
}
}
if (CC_EDITOR || isScene(asset)) {
var id = cc.loader._getReferenceKey(uuid);
Loader.removeItem(id);
}
}
if (callback) {
callback(error, asset);
}
});
},
場景執行與切換
runSceneImmediate做的事情非常多,大概可以分為以下幾個事情(雖然Creator2.x的runSceneImmediate方法寫法有些變化,但大體做的事情類似):
- 新場景的初始化(_load方法)
- 將持久節點從舊場景挪到新場景中
- 銷燬舊場景、自動釋放應該釋放的資源(舊場景中標記為自動釋放且新場景中沒有引用到的資源)
- 一系列場景切換流程的回撥和事件執行
- 開始啟動場景的回撥和事件
- 啟用並執行新場景
- 場景啟動完成的回撥和事件
runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
const console = window.console; // should mangle
const INIT_SCENE = CC_DEBUG ? 'InitScene' : 'I';
const AUTO_RELEASE = CC_DEBUG ? 'AutoRelease' : 'AR';
const DESTROY = CC_DEBUG ? 'Destroy' : 'D';
const ATTACH_PERSIST = CC_DEBUG ? 'AttachPersist' : 'AP';
const ACTIVATE = CC_DEBUG ? 'Activate' : 'A';
// 場景的初始化,scene._load會呼叫CCNode的_onBatchCreated
// 1. PrefabHelper.syncWithPrefab(this); 大多數情況下會跳過
// 2. _updateDummySgNode將自己的屬性同步給sgNode,並確保sgNode是自己的子節點
// 3. 如果當前節點未啟用,則呼叫ActionManager和EventManager的pauseTarget
// 4. 遍歷子節點呼叫它們的_onBatchCreated
if (scene instanceof cc.Scene) {
console.time(INIT_SCENE);
scene._load(); // ensure scene initialized
console.timeEnd(INIT_SCENE);
}
// detach persist nodes
// 將持久節點從舊場景中移除,並暫時儲存到persistNodeList中
var game = cc.game;
var persistNodeList = Object.keys(game._persistRootNodes).map(function (x) {
return game._persistRootNodes[x];
});
for (let i = 0; i < persistNodeList.length; i++) {
let node = persistNodeList[i];
game._ignoreRemovePersistNode = node;
node.parent = null;
game._ignoreRemovePersistNode = null;
}
var oldScene = this._scene;
// auto release assets
// 呼叫autoRelease進行資源釋放,傳入舊場景資源和新場景資源進行對比釋放
// 當一個資源【勾選了自動釋放且沒有被新場景引用到時】就會被釋放
console.time(AUTO_RELEASE);
var autoReleaseAssets = oldScene && oldScene.autoReleaseAssets && oldScene.dependAssets;
AutoReleaseUtils.autoRelease(autoReleaseAssets, scene.dependAssets, persistNodeList);
console.timeEnd(AUTO_RELEASE);
// unload scene
// 釋放舊的場景,銷燬所有子節點和元件
console.time(DESTROY);
if (cc.isValid(oldScene)) {
oldScene.destroy();
}
this._scene = null;
// purge destroyed nodes belongs to old scene
cc.Object._deferredDestroy();
console.timeEnd(DESTROY);
// 執行開始載入場景回撥並觸發對應的事件(其實這裡應該是啟動場景)
if (onBeforeLoadScene) {
onBeforeLoadScene();
}
this.emit(cc.Director.EVENT_BEFORE_SCENE_LAUNCH, scene);
var sgScene = scene;
// Run an Entity Scene
if (scene instanceof cc.Scene) {
this._scene = scene;
sgScene = scene._sgNode;
// Re-attach or replace persist nodes
// 重新新增持久節點到新場景中,如果發現新場景有相同的節點,這裡會執行一個替換的操作
console.time(ATTACH_PERSIST);
for (let i = 0; i < persistNodeList.length; i++) {
let node = persistNodeList[i];
var existNode = scene.getChildByUuid(node.uuid);
if (existNode) {
// scene also contains the persist node, select the old one
var index = existNode.getSiblingIndex();
existNode._destroyImmediate();
scene.insertChild(node, index);
}
else {
node.parent = scene;
}
}
// 啟用新場景
console.timeEnd(ATTACH_PERSIST);
console.time(ACTIVATE);
scene._activate();
console.timeEnd(ACTIVATE);
}
// Run or replace rendering scene
// 啟動或替換場景
if (!this.getRunningScene()) {
this.runWithScene(sgScene);
}
else {
this.replaceScene(sgScene);
}
// 執行場景啟動完成的回撥,並觸發事件
if (onLaunched) {
onLaunched(null, scene);
}
this.emit(cc.Director.EVENT_AFTER_SCENE_LAUNCH, scene);
},
autoRelease傳入2個場景的資源,以及持久節點,自動釋放掉應該自動釋放的資源(下個場景和持久節點引用到的資源不會被釋放,標記為自動釋放的資源會被釋放)
autoRelease: function (oldSceneAssets, nextSceneAssets, persistNodes) {
var releaseSettings = cc.loader._autoReleaseSetting;
var excludeMap = JS.createMap();
// collect next scene assets
// 收集下一個場景所需的資源
if (nextSceneAssets) {
for (let i = 0; i < nextSceneAssets.length; i++) {
excludeMap[nextSceneAssets[i]] = true;
}
}
// collect assets used by persist nodes
// 收集常駐節點引用的資源
for (let i = 0; i < persistNodes.length; i++) {
visitNode(persistNodes[i], excludeMap)
}
// remove ununsed scene assets
// 移除舊場景中不再使用的資源
if (oldSceneAssets) {
for (let i = 0; i < oldSceneAssets.length; i++) {
let key = oldSceneAssets[i];
if (releaseSettings[key] !== false && !excludeMap[key]) {
cc.loader.release(key);
}
}
}
// remove auto release assets
// (releasing asset will change _autoReleaseSetting, so don't use for-in)
// 釋放標記了auto release的資源
var keys = Object.keys(releaseSettings);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
if (releaseSettings[key] === true && !excludeMap[key]) {
cc.loader.release(key);
}
}
},
cc.loader.release的實現如下,release並不會去釋放它依賴的資源,只是釋放這個資源本身。將資源從cc.loader中移除,如果該資源的content是一個cc.Asset,會呼叫它的release、並release其rawUrls對應的資源。如果是紋理則會呼叫cc.textureCache.removeTextureForKey進行移除,而聲音型別的資源會執行cc.audioEngine.uncache進行釋放。
proto.release = function (asset) {
if (Array.isArray(asset)) {
for (let i = 0; i < asset.length; i++) {
var key = asset[i];
this.release(key);
}
} else if (asset) {
var id = this._getReferenceKey(asset);
var item = this.getItem(id);
if (item) {
var removed = this.removeItem(id);
asset = item.content;
if (asset instanceof cc.Asset) {
if (CC_JSB && asset instanceof cc.SpriteFrame && removed) {
// for the "Temporary solution" in deserialize.js
asset.release();
}
var urls = asset.rawUrls;
for (let i = 0; i < urls.length; i++) {
this.release(urls[i]);
}
} else if (asset instanceof cc.Texture2D) {
cc.textureCache.removeTextureForKey(item.rawUrl || item.url);
} else if (AUDIO_TYPES.indexOf(item.type) !== -1) {
cc.audioEngine.uncache(item.rawUrl || item.url);
}
if (CC_DEBUG && removed) {
this._releasedAssetChecker_DEBUG.setReleased(item, id);
}
}
}
};
cc.loader._autoReleaseSetting記錄了所有資源是否會自動釋放。通過cc.loader.setAutoRelease或setAutoReleaseRecursively可以控制是否自動釋放。
proto.setAutoRelease = function (assetOrUrlOrUuid, autoRelease) {
var key = this._getReferenceKey(assetOrUrlOrUuid);
if (key) {
this._autoReleaseSetting[key] = !!autoRelease;
}
else if (CC_DEV) {
cc.warnID(4902);
}
};
proto.setAutoReleaseRecursively = function (assetOrUrlOrUuid, autoRelease) {
autoRelease = !!autoRelease;
var key = this._getReferenceKey(assetOrUrlOrUuid);
if (key) {
this._autoReleaseSetting[key] = autoRelease;
var depends = AutoReleaseUtils.getDependsRecursively(key);
for (var i = 0; i < depends.length; i++) {
var depend = depends[i];
this._autoReleaseSetting[depend] = autoRelease;
}
}
else if (CC_DEV) {
cc.warnID(4902);
}
};
所有loadRes載入進來的資源都會自動執行setAutoReleaseRecursively(uuid, false),如果我們將某個資源設定為自動釋放,然後用loadRes載入了一個依賴了該資源的新資源,之前的自動釋放設定會被覆蓋。
預載入場景
preloadScene的實現非常簡單,拿到場景資訊之後觸發EVENT_BEFORE_SCENE_LOADING事件並呼叫cc.loader.load載入資源。這個流程與切換場景並不衝突,只是讓場景資源載入的這個流程提前了而已,預載入的場景就算不是接下來要切換的場景,也不會衝突,但可能造成效能和記憶體的浪費。
Creator2.x的preloadScene比1.x多了一個onProgress引數,在cc.loader.load的時候傳入。
preloadScene: function (sceneName, onLoaded) {
var info = this._getSceneUuid(sceneName);
if (info) {
this.emit(cc.Director.EVENT_BEFORE_SCENE_LOADING, sceneName);
cc.loader.load({ uuid: info.uuid, type: 'uuid' }, function (error, asset) {
if (error) {
cc.errorID(1210, sceneName, error.message);
}
if (onLoaded) {
onLoaded(error, asset);
}
});
} else {
var error = 'Can not preload the scene "' + sceneName + '" because it is not in the build settings.';
onLoaded(new Error(error));
cc.error('preloadScene: ' + error);
}
},