LoaderManager 類用於用於批量加載資源。此類是單例,不要手動實例化此類,請通過Laya.loader訪問。全部隊列加載完成,會派發 Event.COMPLETE 事件;如果隊列中任意一個加載失敗,會派發 Event.ERROR 事件,事件回調參數值為加載出錯的資源地址。
LoaderManager 類提供了以下幾種功能: 多線程:默認5個加載線程,可以通過maxLoader屬性修改線程數量; 多優先級:有0-4共5個優先級,優先級高的優先加載。0最高,4最低; 重復過濾:自動過濾重復加載(不會有多個相同地址的資源同時加載)以及復用緩存資源,防止重復加載; 錯誤重試:資源加載失敗后,會重試加載(以最低優先級插入加載隊列),retryNum設定加載失敗后重試次數,retryDelay設定加載重試的時間間隔。
LoaderManager是Laya加載資源的統一入口,管理laya的加載器(Laya.Loader),負責控制加載優先級,加載線程維護與上限上線控制,加載失敗后重試等操作。在laya中使用的Laya.loader是LoaderManager的實例,而不是Laya.Loader的實例。
源碼閱讀
屬性簡介
/**@private */
private static var _resMap:Object = {}; //@資源map,url做key,resInfo為value, 用於檢測資源是否已經在隊列中,防止重復加載。
/**@private */
public static var createMap:Object = {atlas: [null, Loader.ATLAS]}; //@用於檢測create方法的類型。
/** 加載出錯后的重試次數,默認重試一次*/
public var retryNum:int = 1;
/** 延遲時間多久再進行錯誤重試,默認立即重試*/
public var retryDelay:int = 0;
/** 最大下載線程,默認為5個*/
public var maxLoader:int = 5;
/**@private */
private var _loaders:Array = []; //@緩存的loader列表
/**@private */
private var _loaderCount:int = 0; //@正在使用的loader數量、加載線程數量
/**@private */
private var _resInfos:Array = []; //@加載資源列表,存放所有需要加載的資源,按優先級和隊列順序排列
/**@private */
private var _infoPool:Array = []; //@加載數據對象的緩存池
/**@private */
private var _maxPriority:int = 5; //@優先級數量 0-4
/**@private */
private var _failRes:Object = {}; //@失敗資源的加載次數,用於失敗后重試計數
初始化
- LoaderManager在Laya.init時創建,賦值給Laya.loader。
- 按照優先級,創建5(_maxPriority)個隊列,用於存放需要加載的資源數據。隊列的索引就是加載的優先級,0-4,數字越小,優先級越高。加載時,按照優先級和隊列順序依次加載。
/**
* <p>創建一個新的 <code>LoaderManager</code> 實例。</p>
* <p><b>注意:</b>請使用Laya.loader加載資源,這是一個單例,不要手動實例化此類,否則會導致不可預料的問題。</p>
*/
public function LoaderManager() {
for (var i:int = 0; i < this._maxPriority; i++) this._resInfos[i] = [];is._resInfos[i] = [];
}
單個資源加載流程
- 判斷資源是否存在,如果存在則直接調用progress和complete。加載的資源存放在Laya.Loader中,不在LoaderManager中。
- 判斷資源是否在加載隊列中:
- 如果存在,則監聽COMPLETE和PROFRESS事件。注:添加時不能設置offBefore,防止后續加載的資源的回調覆蓋之前的加載完成事件。
- 如果不存在,則創建ResInfo,將info放到對應優先級隊列的末尾,監聽COMPLETE事件。調用next方法,嘗試加載。
- 檢測當前正在使用的loader的數量,如果數量不足maxLoader個數,則按優先級查找隊列,再取隊列的第一個數據去加載。如果當前正在使用的loader的count為0,則觸發Laya.loader(LoaderManager)的COMPLETE事件。
- 加載是使用Laya.Loader執行加載邏輯。
- 加載完成后(或者失敗),回收當前loader,更新loader數量,如果加載失敗,則重新放入加載隊列,並且優先級設置為最低。如果失敗多次,則出發Error事件,將url傳遞出去。加載成功或者失敗都會出發COMPLETE事件,調用complete方法。清理和回收相關數據。
- 嘗試調用next,嘗試選擇下一個資源加載。
/**
* <p>加載資源。資源加載錯誤時,本對象會派發 Event.ERROR 事件,事件回調參數值為加載出錯的資源地址。</p>
* <p>因為返回值為 LoaderManager 對象本身,所以可以使用如下語法:Laya.loader.load(...).load(...);</p>
* @param url 要加載的單個資源地址或資源信息數組。比如:簡單數組:["a.png","b.png"];復雜數組[{url:"a.png",type:Loader.IMAGE,size:100,priority:1},{url:"b.json",type:Loader.JSON,size:50,priority:1}]。
* @param complete 加載結束回調。根據url類型不同分為2種情況:1. url為String類型,也就是單個資源地址,如果加載成功,則回調參數值為加載完成的資源,否則為null;2. url為數組類型,指定了一組要加載的資源,如果全部加載成功,則回調參數值為true,否則為false。
* @param progress 加載進度回調。回調參數值為當前資源的加載進度信息(0-1)。
* @param type 資源類型。比如:Loader.IMAGE。
* @param priority (default = 1)加載的優先級,優先級高的優先加載。有0-4共5個優先級,0最高,4最低。
* @param cache 是否緩存加載結果。
* @param group 分組,方便對資源進行管理。
* @param ignoreCache 是否忽略緩存,強制重新加載。
* @return 此 LoaderManager 對象本身。
*/
public function load(url:*, complete:Handler = null, progress:Handler = null, type:String = null, priority:int = 1, cache:Boolean = true, group:String = null, ignoreCache:Boolean = false):LoaderManager {
if (url is Array) return _loadAssets(url as Array, complete, progress, type, priority, cache, group);
var content:* = Loader.getRes(url);
if (content != null) {
//增加延遲回掉
Laya.timer.frameOnce(1, null, function():void {
progress && progress.runWith(1);
complete && complete.runWith(content);
//判斷是否全部加載,如果是則拋出complete事件
_loaderCount || event(Event.COMPLETE);
});
} else {
var info:ResInfo = _resMap[url];
if (!info) {
info = _infoPool.length ? _infoPool.pop() : new ResInfo();
info.url = url;
info.type = type;
info.cache = cache;
info.group = group;
info.ignoreCache = ignoreCache;
//@收到ResInfo的COMPLETE事件則執行complete方法
complete && info.on(Event.COMPLETE, complete.caller, complete.method, complete.args);
progress && info.on(Event.PROGRESS, progress.caller, progress.method, progress.args);
_resMap[url] = info;
//@選擇對應優先級隊列
priority = priority < this._maxPriority ? priority : this._maxPriority - 1;
this._resInfos[priority].push(info);
_next();
} else {
//@如果已經在加載隊列,則只注冊事件,不創建加載數據。
//@注:這里offBefore設置為false,防止事件被覆蓋,導致complete執行不全。
complete && info._createListener(Event.COMPLETE, complete.caller, complete.method, complete.args, false, false);
progress && info._createListener(Event.PROGRESS, progress.caller, progress.method, progress.args, false, false);
}
}
return this;
}
private function _next():void {
//@loader滿了則返回等待空閑loader
if (this._loaderCount >= this.maxLoader) return;
//@按優先級查找列表
for (var i:int = 0; i < this._maxPriority; i++) {
var infos:Array = this._resInfos[i];
while (infos.length > 0) {
var info:ResInfo = infos.shift();
if (info) return _doLoad(info);
}
}
//@所有loader都空閑,則觸發LoaderManager的加載完成
_loaderCount || event(Event.COMPLETE);
}
private function _doLoad(resInfo:ResInfo):void {
this._loaderCount++;
var loader:Loader = this._loaders.length ? this._loaders.pop() : new Loader();
loader.on(Event.COMPLETE, null, onLoaded);
loader.on(Event.PROGRESS, null, function(num:Number):void {
resInfo.event(Event.PROGRESS, num);
});
loader.on(Event.ERROR, null, function(msg:*):void {
onLoaded(null);
});
var _this:LoaderManager = this;
function onLoaded(data:* = null):void {
//@清理、回收loader
loader.offAll();
loader._data = null;
loader._customParse = false;
_this._loaders.push(loader);
_this._endLoad(resInfo, data is Array ? [data] : data);
_this._loaderCount--;
_this._next();
}
//@具體加載流程在Loader中實現
loader._class = resInfo.clas;
loader.load(resInfo.url, resInfo.type, resInfo.cache, resInfo.group, resInfo.ignoreCache);
}
private function _endLoad(resInfo:ResInfo, content:*):void {
//如果加載后為空,放入隊列末尾重試
var url:String = resInfo.url;
if (content == null) {
var errorCount:int = this._failRes[url] || 0;
//@記錄失敗次數
if (errorCount < this.retryNum) {
console.warn("[warn]Retry to load:", url);
this._failRes[url] = errorCount + 1;
//@重新放回加載隊列
Laya.timer.once(retryDelay, this, _addReTry, [resInfo], false);
return;
} else {
//超過retryNum則直接拋出ERROR事件
console.warn("[error]Failed to load:", url);
event(Event.ERROR, url);
}
}
if (_failRes[url]) _failRes[url] = 0;
delete _resMap[url];
//@通知info執行complete方法
resInfo.event(Event.COMPLETE, content);
resInfo.offAll();
_infoPool.push(resInfo);
}
private function _addReTry(resInfo:ResInfo):void {
this._resInfos[this._maxPriority - 1].push(resInfo);
_next();
}
加載多個資源流程
LoaderManager.load支持傳入簡單類型數組,比如:簡單數組:["a.png","b.png"];復雜數組[{url:"a.png",type:Loader.IMAGE,size:100,priority:1},{url:"b.json",type:Loader.JSON,size:50,priority:1}]。
- 遍歷所有數組,記錄總數,用於計算progress。記錄下載的總數。
- 分別調用load方法,加載每一個元素。
- 每加載完一個資源,檢查加載的數量是否等於本次的總數量,如果是,則執行complete操作。
/**
* @private
* 加載數組里面的資源。
* @param arr 簡單:["a.png","b.png"],復雜[{url:"a.png",type:Loader.IMAGE,size:100,priority:1},{url:"b.json",type:Loader.JSON,size:50,priority:1}]*/
private function _loadAssets(arr:Array, complete:Handler = null, progress:Handler = null, type:String = null, priority:int = 1, cache:Boolean = true, group:String = null):LoaderManager {
var itemCount:int = arr.length;
var loadedCount:int = 0;
var totalSize:int = 0;
var items:Array = [];
var success:Boolean = true;
for (var i:int = 0; i < itemCount; i++) {
var item:Object = arr[i];
if (item is String) item = {url: item, type: type, size: 1, priority: priority};
if (!item.size) item.size = 1;
item.progress = 0;
totalSize += item.size;
items.push(item);
var progressHandler:* = progress ? Handler.create(null, loadProgress, [item], false) : null;
var completeHandler:* = (complete || progress) ? Handler.create(null, loadComplete, [item]) : null;
load(item.url, completeHandler, progressHandler, item.type, item.priority || 1, cache, item.group || group);
}
function loadComplete(item:Object, content:* = null):void {
loadedCount++;
item.progress = 1;
if (!content) success = false;
if (loadedCount === itemCount && complete) {
complete.runWith(success);
}
}
function loadProgress(item:Object, value:Number):void {
if (progress != null) {
item.progress = value;
var num:Number = 0;
for (var j:int = 0; j < items.length; j++) {
var item1:Object = items[j];
num += item1.size * item1.progress;
}
var v:Number = num / totalSize;
progress.runWith(v);
}
}
return this;
}
create方法
根據clas類型創建一個未初始化資源的對象,隨后進行異步加載,資源加載完成后,初始化對象的資源,並通過此對象派發 Event.LOADED 事件,事件回調參數值為此對象本身。套嵌資源的子資源會保留資源路徑"?"后的部分。
如果url為數組,返回true;否則返回指定的資源類對象,可以通過偵聽此對象的 Event.LOADED 事件來判斷資源是否已經加載完畢。
注意:cache參數只能對文件后綴為atlas的資源進行緩存控制,其他資源會忽略緩存,強制重新加載。
-
create方法可以先創建一個指定類型的殼,同步返回給調用者。殼里的資源則通過異步的方式加載。
-
create方法主要是在Laya3D中使用,主要是Laya內部組件使用。使用create創建的類型有嚴格限制,在2D下支持Atlas,在3D有一定擴展。
-
這些類型都要實現ICreateResource方法,提供onAsynLoaded,_setUrl等方法。如果創建一些不支持的類型,會直接拋出異常。
-
同load方法一樣,支持傳入一個或者多個資源地址。
-
使用create方法加載Atlas資源時,會返回null。
-
Laya3D所支持的類型:
//在Laya3D初始化時設置
var createMap:Object = LoaderManager.createMap;
createMap["lh"] = [Sprite3D, Laya3D.HIERARCHY];
createMap["ls"] = [Scene, Laya3D.HIERARCHY];
createMap["lm"] = [Mesh, Laya3D.MESH];
createMap["lmat"] = [StandardMaterial, Laya3D.MATERIAL];
createMap["lpbr"] = [PBRMaterial, Laya3D.MATERIAL];
createMap["ltc"] = [TextureCube, Laya3D.TEXTURECUBE];
createMap["jpg"] = [Texture2D, "nativeimage"];
createMap["jpeg"] = [Texture2D, "nativeimage"];
createMap["png"] = [Texture2D, "nativeimage"];
createMap["pkm"] = [Texture2D, Loader.BUFFER];
createMap["lsani"] = [AnimationTemplet, Loader.BUFFER];
createMap["lrani"] = [AnimationTemplet, Loader.BUFFER];
createMap["raw"] = [DataTexture2D, Loader.BUFFER];
createMap["mipmaps"] = [DataTexture2D, Loader.BUFFER];
createMap["thdata"] = [TerrainHeightData, Loader.BUFFER];
createMap["lt"] = [TerrainRes, Laya3D.TERRAIN];
createMap["lani"] = [AnimationClip, Loader.BUFFER];
createMap["lav"] = [Avatar, Loader.JSON];
createMap["ani"] = [AnimationTemplet, Loader.BUFFER];//兼容接口