Load流程是整个资源加载管线的最后一棒,由Loader这个pipe负责(loader.js)。经过Download流程拿到内容以后,须要对内容作一些“加载”处理。使得这些内容能够在游戏中使用。这里并非全部的资源都须要进行一个加载处理,目前只有图片、Json、Plist、Uuid(Prefab、场景)等资源才会执行加载的流程,其余的资源在Download流程以后就能够在游戏中使用了。json
Loader的handle接收一个item和callback,根据item的type在this.extMap中获取对应的loadFunc。数组
Loader.prototype.addHandlers = function (extMap) { this.extMap = JS.mixin(this.extMap, extMap); }; Loader.prototype.handle = function (item, callback) { var loadFunc = this.extMap[item.type] || this.extMap['default']; return loadFunc.call(this, item, callback); };
Loader的this.extMap记录了各类资源类型的下载方式,全部的类型最终都对应这5个加载方法,loadNothing、loadJSON、loadImage、loadPlist、loadUuid,它们对应实现了各类类型资源的加载,经过Loader.addHandlers能够添加或修改任意资源的加载方式。加载结束后将可用的内容返回。缓存
// 无需加载,即通过前面的下载已经可用了,例如font、script等资源 function loadNothing (item, callback) { return null; } // 使用JSON.parse进行解析并返回 function loadJSON (item, callback) { if (typeof item.content !== 'string') { return new Error('JSON Loader: Input item doesn\'t contain string content'); } try { var result = JSON.parse(item.content); return result; } catch (e) { return new Error('JSON Loader: Parse json [' + item.id + '] failed : ' + e); } } // 建立Texture2D,并根据图片的内容初始化Texture2D,最后添加到cc.textureCache中 function loadImage (item, callback) { if (sys.platform !== sys.WECHAT_GAME && !(item.content instanceof Image)) { return new Error('Image Loader: Input item doesn\'t contain Image content'); } var rawUrl = item.rawUrl; var tex = cc.textureCache.getTextureForKey(rawUrl) || new Texture2D(); tex.url = rawUrl; tex.initWithElement(item.content); tex.handleLoadedTexture(); cc.textureCache.cacheImage(rawUrl, tex); return tex; } // 使用cc.plistParser进行解析并返回 function loadPlist (item, callback) { if (typeof item.content !== 'string') { return new Error('Plist Loader: Input item doesn\'t contain string content'); } var result = cc.plistParser.parse(item.content); if (result) { return result; } else { return new Error('Plist Loader: Parse [' + item.id + '] failed'); } }
loadUuid用于加载creator内部统一规划的资源,每一个uuid都会对应一个json对象,多是prefab、spriteFrame,等等。在loadUuid这个方法中,最关键的操做就是cc.deserialize反序列化把资源对象建立了出来,其次就是加载依赖资源。app
uuid的解析首先须要一个json对象,若是item的content是string类型,则进行解析,若是是object类型,则直接使用item.content,若是既不是string也不是object则直接报错。异步
function loadUuid (item, callback) { if (CC_EDITOR) { MissingClass = MissingClass || Editor.require('app://editor/page/scene-utils/missing-class-reporter').MissingClass; } // 获取json对象,若是是string则进行解析,若是是object则直接使用,报错则返回Error对象 var json; if (typeof item.content === 'string') { try { json = JSON.parse(item.content); } catch (e) { return new Error('Uuid Loader: Parse asset [' + item.id + '] failed : ' + e.stack); } } else if (typeof item.content === 'object') { json = item.content; } else { return new Error('JSON Loader: Input item doesn\'t contain string content'); } // 根据是否场景对象、编辑器环境来决定classFinder的实现。 var classFinder; var isScene = isSceneObj(json); if (isScene) { if (CC_EDITOR) { // 编辑器 + 场景的模式下,使用MissingClass.classFinder做为包裹函数 MissingClass.hasMissingClass = false; classFinder = function (type, data, owner, propName) { var res = MissingClass.classFinder(type, data, owner, propName); if (res) { return res; } return cc._MissingScript.getMissingWrapper(type, data); }; classFinder.onDereferenced = MissingClass.classFinder.onDereferenced; } else { // 非编辑器下,使用cc._MissingScript.safeFindClass,也是调用了JS._getClassById // 区别是在解析失败后会调用cc.deserialize.reportMissingClass(id); classFinder = cc._MissingScript.safeFindClass; } } else { classFinder = function (id) { // JS为引擎的platform\js.js,而_getClassById方法从_idToClass[classId]中返回Class // _idToClass为id到类的一个注册map,key为id,value为class // 使用CCClass定义继承自cc.Component的类会被自动注册到_idToClass中 // platform\CCClass.js中的var cls = define(name, base, mixins, options); // 最终调用了JS.setClassName,Creator的类的实现细节是另一个大话题 // 这里只须要了解,全部可拖拽到prefab上的类都会被注册到JS._idToClass中,这里的id就是类名 var cls = JS._getClassById(id); if (cls) { return cls; } cc.warnID(4903, id); return Object; }; } // 进行反序列化,反序列化出asset var tdInfo = cc.deserialize.Details.pool.get(); var asset; try { // deserialize的实现位于platform\deserialize.js // 具体的实现很是复杂,大体能够理解为new出对应的类,并从json对象中反序列化该类的全部属性 // 因此返回的asset是这个json最顶层object对应的类,好比cc.SpriteFrame或者自定义的组件 // 该资源所依赖的全部资源会被反序列化到tdInfo中,在tdInfo.uuidList中。 asset = cc.deserialize(json, tdInfo, { classFinder: classFinder, target: item.existingAsset, customEnv: item }); } catch (e) { cc.deserialize.Details.pool.put(tdInfo); var err = CC_JSB ? (e + '\n' + e.stack) : e.stack; return new Error('Uuid Loader: Deserialize asset [' + item.id + '] failed : ' + err); } // 若是是在编辑器下的场景存在类丢失,进行报告(应该是报红) asset._uuid = item.uuid; if (CC_EDITOR && isScene && MissingClass.hasMissingClass) { MissingClass.reportMissingClass(asset); } // 判断是否可延迟加载,并调用loadDepends var deferredLoad = canDeferredLoad(asset, item, isScene); loadDepends(this.pipeline, item, asset, tdInfo, deferredLoad, callback); }
canDeferredLoad方法会根据资源类型监测是否能够延迟加载,当item的deferredLoadRaw为true且该资源支持延迟加载(在代码中搜索preventDeferredLoadDependents能够发现除了TileMap、DragonBones、Spine等资源外,都不支持延迟加载),或是设置了延迟加载的场景才能够延迟加载。async
// can deferred load raw assets in runtime // 检查是否延迟加载Raw Assets function canDeferredLoad (asset, item, isScene) { if (CC_EDITOR || CC_JSB) { return false; } var res = item.deferredLoadRaw; if (res) { // check if asset support deferred // 检查该资源是否支持延迟加载 if (asset instanceof cc.Asset && asset.constructor.preventDeferredLoadDependents) { res = false; } } else if (isScene) { // 若是是prefab或scene,取其asyncLoadAssets属性 if (asset instanceof cc.SceneAsset || asset instanceof cc.Prefab) { res = asset.asyncLoadAssets; } } return res; }
loadDepends方法会加载依赖,主要作了2个事情,延迟加载和依赖加载。编辑器
延迟加载指的是资源A依赖了B、C、D,其中资源D延迟加载了,那么BC加载完成即算这个资源加载完成,并执行回调,D也会进行加载,但何时加载完这里并不关心。在实际应用中的表现就是加载一个场景,基础部分的内容加载完成了,进入了该场景以后再陆续看到其余内容加载完成。函数
根据deferredLoadRawAssetsInRuntime,对raw类型资源进行延迟加载,延迟加载的内容会进入dependKeys数组,而不延迟加载的内容进入depends数组。ui
depends数组是该资源所依赖的资源数组,loadDepends会调用pipeline.flowInDeps进行加载,若是该数组为空则不加载依赖,执行完成回调。dependKeys数组是item的属性,记录了该资源依赖的全部资源,在作资源释放的时候会用到。预加载的内容会直接进入dependKeys,而正常加载的资源在加载完成后才会被添加到dependKeys中。this
最后调用pipeline.flowInDeps加载depends数组,flowInDeps的完成回调中,若是item加载完成且没有报错,调用loadCallback,若是未加载完成,插入到item的queue的 _callbackTable[dependSrc]中或添加queue的监听(这两个操做的意义都是在item加载完成后执行loadCallback),loadCallback将依赖对象的依赖属性进行赋值,并添加该资源的id到dependKeys中。
当反序列化出来的asset._preloadRawFiles有值时,会将callback进行包裹,在异步加载完RawFiles才执行最终的callback。实际并无什么做用。
function loadDepends (pipeline, item, asset, tdInfo, deferredLoadRawAssetsInRuntime, callback) { // tdInfo.uuidList为这个prefab或场景所依赖的uuid类型的资源 var uuidList = tdInfo.uuidList; var objList, propList, depends; var i, dependUuid; // cache dependencies for auto release // dependKeys用于缓存该资源的依赖,在资源释放的时候会用到 var dependKeys = item.dependKeys = []; /******************************* 过滤决定哪些资源要加载,哪些要延迟,得出depends数组 **********************************/ // 若是支持延迟加载 if (deferredLoadRawAssetsInRuntime) { objList = []; propList = []; depends = []; // parse depends assets for (i = 0; i < uuidList.length; i++) { dependUuid = uuidList[i]; var obj = tdInfo.uuidObjList[i]; var prop = tdInfo.uuidPropList[i]; var info = cc.AssetLibrary._getAssetInfoInRuntime(dependUuid); if (info.raw) { // skip preloading raw assets // 对于raw类型的资源不进行加载,tdInfo.uuidObjList[i][prop] = url var url = info.url; obj[prop] = url; dependKeys.push(url); } else { objList.push(obj); propList.push(prop); // declare depends assets // 对于非raw类型的资源,进入depends进行加载,但带上deferredLoadRaw标记 // 意为该uuid引用的其余raw类型的资源进行延迟加载 depends.push({ type: 'uuid', uuid: dependUuid, deferredLoadRaw: true, }); } } } else { objList = tdInfo.uuidObjList; propList = tdInfo.uuidPropList; depends = new Array(uuidList.length); // declare depends assets // 不支持延迟加载则直接进入depends数组,这里没有deferredLoadRaw标记 for (i = 0; i < uuidList.length; i++) { dependUuid = uuidList[i]; depends[i] = { type: 'uuid', uuid: dependUuid }; } } /******************************* tdInfo.rawProp和asset._preloadRawFiles的处理 **********************************/ // declare raw // 有些json文件包含了一些raw属性,以$_$rawType结尾,这里会直接加载item.url,但目前还未碰到过这样类型的资源。 // 下面2个说法是错误的。 // 若是这个uuid资源自己就是一个raw资源,加载本身? // 若是这个uuid资源存在raw属性,例如一个脚本拖拽了一个Texture2D类型的资源做为它的成员变量? if (tdInfo.rawProp) { objList.push(asset); propList.push(tdInfo.rawProp); depends.push(item.url); } // preload raw files // 预加载它的raw文件,这里是asset的属性,但从引擎代码没有看到哪里对这个属性赋值过 // 不过prefab等文件却是有一个_rawFiles的属性,但从代码上看也与这个方法无关,看上去倒像是预留的一个接口 // 提供给开发者作某种资源类型的完成回调包装。 if (asset._preloadRawFiles) { var finalCallback = callback; callback = function () { asset._preloadRawFiles(function (err) { finalCallback(err || null, asset); }); }; } // fast path // 若是没有资源要加载就直接返回 if (depends.length === 0) { cc.deserialize.Details.pool.put(tdInfo); return callback(null, asset); } /******************************* 调用pipeline.flowInDeps进行依赖加载,资源加载完成后调用loadCallback **********************************/ // Predefine content for dependencies usage // 加载depends,加载完成后注册到item.dependKeys中,并赋值给this.obj[this.prop] item.content = asset; pipeline.flowInDeps(item, depends, function (errors, items) { // 这个回调在全部的item都加载完成后执行,因此item都是有的,但有可能有报错 var item, missingAssetReporter; for (var src in items.map) { item = items.map[src]; if (item.uuid && item.content) { item.content._uuid = item.uuid; } } for (var i = 0; i < depends.length; i++) { var dependSrc = depends[i].uuid; var dependUrl = depends[i].url; var dependObj = objList[i]; var dependProp = propList[i]; item = items.map[dependUrl]; if (item) { var thisOfLoadCallback = { obj: dependObj, prop: dependProp }; // 资源加载完成的回调,关联依赖对象obj的prop为item的value function loadCallback (item) { var value = item.isRawAsset ? item.rawUrl : item.content; this.obj[this.prop] = value; if (item.uuid !== asset._uuid && dependKeys.indexOf(item.id) < 0) { dependKeys.push(item.id); } } // 若是资源已经加载完了,且没有报错,则执行loadCallback回调 if (item.complete || item.content) { if (item.error) { if (CC_EDITOR && item.error.errorCode === 'db.NOTFOUND') { if (!missingAssetReporter) { var MissingObjectReporter = Editor.require('app://editor/page/scene-utils/missing-object-reporter'); missingAssetReporter = new MissingObjectReporter(asset); } missingAssetReporter.stashByOwner(dependObj, dependProp, Editor.serialize.asAsset(dependSrc)); } else { cc._throw(item.error); } } else { loadCallback.call(thisOfLoadCallback, item); } } else { // item was removed from cache, but ready in pipeline actually // 该item从cache中移除了?但在pipeline中? // 这里监听了该item的加载完成事件,在加载完成时调用loadCallback var queue = LoadingItems.getQueue(item); // Hack to get a better behavior // 这个behavior很是的bad,_callbackTable是CallbacksHandler的成员变量 // 两个操做都是添加监听,但前者是直接拿到监听该事件的回调数组,强行插入 var list = queue._callbackTable[dependSrc]; if (list) { list.unshift(loadCallback, thisOfLoadCallback); } else { queue.addListener(dependSrc, loadCallback, thisOfLoadCallback); } } } } if (CC_EDITOR && missingAssetReporter) { missingAssetReporter.reportByOwner(); } cc.deserialize.Details.pool.put(tdInfo); callback(null, asset); }); }
CCLoader的flowInDeps,实现以下,传入资源的owner,依赖列表urlList,以及urlList的回调。当一个依赖又有依赖的时候,queue的append又会走到这个新资源的loadUuid,去加载那一层所依赖的资源。而flowInDeps开头的var item = this._cache[res.url] 也确保了资源不会被重复加载。
proto.flowInDeps = function (owner, urlList, callback) { // 准备_sharedList,已加载或正在加载的资源push item,未加载的push res _sharedList.length = 0; for (var i = 0; i < urlList.length; ++i) { var res = getResWithUrl(urlList[i]); if (!res.url && ! res.uuid) continue; var item = this._cache[res.url]; if (item) { _sharedList.push(item); } else { _sharedList.push(res); } } // 建立一个新的队列,当有owner时,将子队列的进度同步到ownerQueue var queue = LoadingItems.create(this, owner ? function (completedCount, totalCount, item) { if (this._ownerQueue && this._ownerQueue.onProgress) { this._ownerQueue._childOnProgress(item); } } : null, function (errors, items) { callback(errors, items); // Clear deps because it's already done // Each item will only flowInDeps once, so it's still safe here // 加载完成后清除owner.deps数组 owner && owner.deps && (owner.deps.length = 0); items.destroy(); }); if (owner) { var ownerQueue = LoadingItems.getQueue(owner); // Set the root ownerQueue, if no ownerQueue defined in ownerQueue, it's the root // 设置queue的ownerQueue queue._ownerQueue = ownerQueue._ownerQueue || ownerQueue; } var accepted = queue.append(_sharedList, owner); _sharedList.length = 0; return accepted; };
在creator编辑器中能够设置场景和prefab的延迟加载,设置了延迟加载以后,场景或prefab所引用的一些Raw类型资源如cc.Texture2D、cc.AudioClip等会延迟加载,同时,玩家进入场景后可能会看到一些资源陆续显示出来,而且激活新界面时也可能会看到界面中的元素陆续显示出来,所以这种加载方式更适合网页游戏。
具体的实现是在loadUuid中执行canDeferredLoad时,它的asset.asyncLoadAssets为一个Object。在后面的loadDepends方法中会执行deferredLoadRawAssetsInRuntime的判断。全部Raw类型的资源会被延迟加载,而非Raw类型的资源会被添加到depends数组中进行加载。最终加载完成时咱们能够获得一个不完整的资源(由于它有一部分依赖被延迟加载了)。
从整个Pipeline的加载流程来看,并无任何地方去加载这些被延迟的Raw类型资源,而在底层加载图片的地方进行断点,能够发现当场景或Prefab被激活时(添加到场景中),会有一个ensureLoadTexture方法被调用,在这里会执行这些延迟资源的加载流程。因此延迟加载的资源在节点被激活时会自动加载。下图是一个延迟加载图片的调用堆栈。
ensureLoadTexture的实现以下所示,AudioClip也相似,在调用play播放声音时会执行preload,检测到声音没有被加载时会执行cc.loader.load方法加载声音。
/** * !#en If a loading scene (or prefab) is marked as `asyncLoadAssets`, all the textures of the SpriteFrame which * associated by user's custom Components in the scene, will not preload automatically. * These textures will be load when Sprite component is going to render the SpriteFrames. * You can call this method if you want to load the texture early. * !#zh 当加载中的场景或 Prefab 被标记为 `asyncLoadAssets` 时,用户在场景中由自定义组件关联到的全部 SpriteFrame 的贴图都不会被提早加载。 * 只有当 Sprite 组件要渲染这些 SpriteFrame 时,才会检查贴图是否加载。若是你但愿加载过程提早,你能够手工调用这个方法。 */ ensureLoadTexture: function () { if (!this._texture) { this._loadTexture(); } }, _loadTexture: function () { if (this._textureFilename) { // 这里返回的tex多是一个未加载完成的纹理,如纹理未加载完成,可监听其加载完成回调 var texture = cc.textureCache.addImage(this._textureFilename); this._refreshTexture(texture); } }, _refreshTexture: function (texture) { var self = this; if (self._texture !== texture) { var locLoaded = texture.loaded; this._textureLoaded = locLoaded; this._texture = texture; function textureLoadedCallback () { if (!self._texture) { // clearTexture called while loading texture... // 在加载纹理的时候调用了clearTexture方法 return; } self._textureLoaded = true; var w = texture.width, h = texture.height; // 若是在Canvas模式下,图片有旋转,须要进行旋转的特殊处理(_cutRotateImageToCanvas) if (self._rotated && cc._renderType === cc.game.RENDER_TYPE_CANVAS) { var tempElement = texture.getHtmlElementObj(); tempElement = _ccsg.Sprite.CanvasRenderCmd._cutRotateImageToCanvas(tempElement, self.getRect()); var tempTexture = new cc.Texture2D(); tempTexture.initWithElement(tempElement); tempTexture.handleLoadedTexture(); self._texture = tempTexture; self._rotated = false; w = self._texture.width; h = self._texture.height; self.setRect(cc.rect(0, 0, w, h)); } if (self._rect) { self._checkRect(self._texture); } else { self.setRect(cc.rect(0, 0, w, h)); } if (!self._originalSize) { self.setOriginalSize(cc.size(w, h)); } if (!self._offset) { self.setOffset(cc.v2(0, 0)); } // dispatch 'load' event of cc.SpriteFrame // cc.SpriteFrame的触发load事件 self.emit("load"); } // 若是图片已加载完,则直接执行回调,不然监听texture的load方法 if (locLoaded) { textureLoadedCallback(); } else { texture.once("load", textureLoadedCallback); } } },
在Creator的官方文档中介绍到“Spine 和 TiledMap 依赖的资源永远都不会被延迟加载”,这主要是由于它们对Raw资源是一个强依赖,也就是说节点被激活时就必须使用到它们的纹理,因此不能延迟加载。那么它们是如何实现禁止延迟加载的呢?
在canDeferredLoad方法中,若是资源的asset.constructor.preventDeferredLoadDependents为true时,会强制返回false。在引擎中进行搜索能够发现,除了Spine和TiledMap,还有DragonBones也是被禁止延迟加载的。