這裏討論場景切換的完整流程,從咱們調用了loadScene開始切換場景,到場景切換完成背後發生的事情。整個流程能夠分爲場景加載和場景切換兩部分,另外還簡單討論了場景的預加載。node
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中沒有區別ui
_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中移除。this
loadAsset方法在Creator2.x和Creator1.x中沒有區別url
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方法寫法有些變化,但大致作的事情相似):code
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); } },