Cocos Creator 資源加載流程剖析【六】——場景切換流程

這裏討論場景切換的完整流程,從咱們調用了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

  • 新場景的初始化(_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);
        }
    },
相關文章
相關標籤/搜索