Laya LoaderManager小记

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];//兼容接口
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容