Download流程的处理由Downloader这个pipe负责(downloader.js),Downloader提供了各类资源的“下载”方式——即如何获取文件内容,有从网络获取、从磁盘获取,不一样类型的资源在不一样的平台下有不一样的获取方式。css
又好比json在原平生台使用jsb.fileutils进行加载,而在H5平台则使用XMLHttpRequest从网络下载。web
Downloader的handle接收一个item和callback,根据item的type在this.extMap中获取对应的downloadFunc,交由downloadFunc下载,根据下载结果调用callback。同时有一个并发限制,默认最多同时下载64个资源,超过的会进入队列,等待前面的资源加载完成后再依次进行加载。若是item的ignoreMaxConcurrency为true则无视该并发限制。downloadFunc接受一个item和一个callback,若是是同步下载,须要返回downloadFunc的返回值,而异步下载则返回undefined或不返回。json
Downloader.prototype.handle = function (item, callback) { var self = this; var downloadFunc = this.extMap[item.type] || this.extMap['default']; var syncRet = undefined; if (this._curConcurrent < cc.macro.DOWNLOAD_MAX_CONCURRENT) { this._curConcurrent++; syncRet = downloadFunc.call(this, item, function (err, result) { self._curConcurrent = Math.max(0, self._curConcurrent - 1); self._handleLoadQueue(); callback && callback(err, result); }); // 当downloadFunc是同步执行的,会返回非undefined的syncRet if (syncRet !== undefined) { this._curConcurrent = Math.max(0, this._curConcurrent - 1); this._handleLoadQueue(); return syncRet; } } else if (item.ignoreMaxConcurrency) { syncRet = downloadFunc.call(this, item, callback); if (syncRet !== undefined) { return syncRet; } } else { this._loadQueue.push({ item: item, callback: callback }); } };
Downloader的this.extMap记录了各类资源类型的下载方式,全部的类型最终都对应这6个下载方法,downloadScript、downloadImage(downloadWebp)、downloadAudio、downloadText、downloadFont、downloadUuid,它们对应实现了各类类型资源的下载,经过Downloader.addHandlers能够添加或修改任意资源的下载方式。跨域
若是是微信或者原平生台,只是对脚本进行require(CommonJS模块化规范),这里主要是web平台的处理,原平生台的处理在后面统一介绍,web平台是经过建立一个script的HTML标签,指定标签的src,添加事件监听,经过这种HTML的方式下载脚本,使其生效。数组
function downloadScript (item, callback, isAsync) { if (sys.platform === sys.WECHAT_GAME) { require(item.url); callback(null, item.url); return; } // 建立一个script标签元素,并指定其src为咱们的源码路径 var url = item.url, d = document, s = document.createElement('script'); s.async = isAsync; s.src = urlAppendTimestamp(url); function loadHandler () { s.parentNode.removeChild(s); s.removeEventListener('load', loadHandler, false); s.removeEventListener('error', errorHandler, false); callback(null, url); } function errorHandler() { s.parentNode.removeChild(s); s.removeEventListener('load', loadHandler, false); s.removeEventListener('error', errorHandler, false); callback(new Error('Load ' + url + ' failed!'), url); } // 添加加载完成和错误回调 s.addEventListener('load', loadHandler, false); s.addEventListener('error', errorHandler, false); d.body.appendChild(s); }
当cc.game.config['noCache']为true时,urlAppendTimestamp会在url的尾部添加当前的时间戳,这会致使每次加载资源时因为url不一样,不会直接使用浏览器的缓存,而是从新获取最新的资源,接下来的各类下载函数中也有urlAppendTimestamp。浏览器
downloadWebp和downloadImage都是用于下载图片资源,downloadWebp只是判断了cc.sys.capabilities.webp是否为true,若是为false表示当前的环境不支持webp,若是支持则直接调用downloadImage进行下载。downloadImage中引入了2个概念,imagePool和crossOrigin,imagePool是一个JS.Pool,它的get方法会返回一个Image对象。若是是非https下的跨域请求,下载失败时会使用不跨域的方式再请求一次。缓存
因为浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不一样即为跨域,如下为跨域的详细描述表格。在web端,使用webgl模式没法直接使用跨域图片,须要服务器配合设置Access-Control-Allow-Origin(Canvas模式容许使用跨域图片)。服务器
当咱们访问跨域资源的时候,可否正确加载图片取决于图片服务器是否开启了跨域支持(Access-Control-Allow-Origin: *),好比 http://tools.itharbors.com/res/logo.png 这个资源的服务器开启了跨域支持,因此能够正确加载,不须要调整客户端加载的代码。微信
那么downloadImage为何要在设置crossOrigin加载失败以后,将crossOrigin设置为null再加载一次呢?由于关闭crossOrigin以后虽然能够加载,但没法准确地捕获错误。在测试中,若是服务器没有开启跨域支持,经过将crossOrigin设置为null确实能够下载到图片,然而在webgl初始化该图片时会报错。网络
function downloadImage (item, callback, isCrossOrigin, img) { if (isCrossOrigin === undefined) { isCrossOrigin = true; } var url = urlAppendTimestamp(item.url); img = img || misc.imagePool.get(); if (isCrossOrigin && window.location.protocol !== 'file:') { img.crossOrigin = 'anonymous'; } else { img.crossOrigin = null; } if (img.complete && img.naturalWidth > 0 && img.src === url) { return img; } else { function loadCallback () { img.removeEventListener('load', loadCallback); img.removeEventListener('error', errorCallback); callback(null, img); } function errorCallback () { img.removeEventListener('load', loadCallback); img.removeEventListener('error', errorCallback); // Retry without crossOrigin mark if crossOrigin loading fails // 若是加载失败,重试的时候img.crossOrigin被置为null // Do not retry if protocol is https, even if the image is loaded, cross origin image isn't renderable. // 若是是https就不重试了,由于就算加载了到了图片也没法渲染 if (window.location.protocol !== 'https:' && img.crossOrigin && img.crossOrigin.toLowerCase() === 'anonymous') { downloadImage(item, callback, false, img); } else { callback(new Error('Load image (' + url + ') failed')); } } // 设置src开始加载图片 img.addEventListener('load', loadCallback); img.addEventListener('error', errorCallback); img.src = url; } }
downloadFont的本质也是经过添加HTML标签,经过div、style标签来实现字体的加载。经过item的name、srcs或name、url、type进行加载。
function _loadFont (name, srcs, type){ // 建立一个类型为text/css的style标签 var doc = document, fontStyle = document.createElement('style'); fontStyle.type = 'text/css'; doc.body.appendChild(fontStyle); // 构建并设置fontStyle的textContent属性 var fontStr = ''; if (isNaN(name - 0)) { fontStr += '@font-face { font-family:' + name + '; src:'; } else { fontStr += '@font-face { font-family:\'' + name + '\'; src:'; } if (srcs instanceof Array) { for (var i = 0, li = srcs.length; i < li; i++) { var src = srcs[i]; type = Path.extname(src).toLowerCase(); fontStr += 'url(\'' + srcs[i] + '\') format(\'' + FONT_TYPE[type] + '\')'; fontStr += (i === li - 1) ? ';' : ','; } } else { type = type.toLowerCase(); fontStr += 'url(\'' + srcs + '\') format(\'' + FONT_TYPE[type] + '\');'; } fontStyle.textContent += fontStr + '}'; // 添加一个试用该字体的div //<div style="font-family: PressStart;">.</div> var preloadDiv = document.createElement('div'); var _divStyle = preloadDiv.style; _divStyle.fontFamily = name; preloadDiv.innerHTML = '.'; _divStyle.position = 'absolute'; _divStyle.left = '-100px'; _divStyle.top = '-100px'; doc.body.appendChild(preloadDiv); } function downloadFont (item, callback) { var url = item.url, type = item.type, name = item.name, srcs = item.srcs; if (name && srcs) { if (srcs.indexOf(url) === -1) { srcs.push(url); } _loadFont(name, srcs); } else { type = Path.extname(url); name = Path.basename(url, type); _loadFont(name, url, type); } if (document.fonts) { document.fonts.load('1em ' + name).then(function () { callback(null, null); }, function(err){ callback(err); }); } else { return null; } }
downloadAudio位于audio-downloader.js中,它会根据item的useDom选项决定使用哪一种声音下载方式:
function downloadAudio (item, callback) { // 浏览器不支持音效 if (formatSupport.length === 0) { return new Error('Audio Downloader: audio not supported on this browser!'); } item.content = item.url; // 若是指定了useDom或者不支持WebAudio,会自动帮咱们切换成DomAudio if (!__audioSupport.WEB_AUDIO || (item.urlParam && item.urlParam['useDom'])) { loadDomAudio(item, callback); } else { loadWebAudio(item, callback); } }
loadWebAudio会使用cc.loader.getXMLHttpRequest下载资源,在onLoad回调中使用sys.__audioSupport.context["decodeAudioData"]()进行解码。
而loadDomAudio则是经过aduio这个HTML标签进行加载和监听。
文本的下载分2中方式,若是是原平生台,会使用jsb.fileUtils.getStringFromFile从磁盘中直接获取,若是是其余普通,会使用cc.loader.getXMLHttpRequest下载。
在Creator2.x以后,这段判断被移到了engine目录的jsb目录下,Creator直接在构建时使用合适的代码,而不是在函数执行中去判断当前是哪一种平台。
if (CC_JSB) { module.exports = function (item, callback) { var url = item.url; var result = jsb.fileUtils.getStringFromFile(url); if (typeof result === 'string' && result) { return result; } else { return new Error('Download text failed: ' + url); } }; } else { var urlAppendTimestamp = require('./utils').urlAppendTimestamp; module.exports = function (item, callback) { var url = item.url; url = urlAppendTimestamp(url); var xhr = cc.loader.getXMLHttpRequest(), errInfo = 'Load ' + url + ' failed!', navigator = window.navigator; xhr.open('GET', url, true); if (/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)) { // IE-specific logic here xhr.setRequestHeader('Accept-Charset', 'utf-8'); xhr.onreadystatechange = function () { if(xhr.readyState === 4) { if (xhr.status === 200 || xhr.status === 0) { callback(null, xhr.responseText); } else { callback({status:xhr.status, errorMessage:errInfo}); } } }; } else { if (xhr.overrideMimeType) xhr.overrideMimeType('text\/plain; charset=utf-8'); xhr.onload = function () { if(xhr.readyState === 4) { if (xhr.status === 200 || xhr.status === 0) { callback(null, xhr.responseText); } else { callback({status:xhr.status, errorMessage:errInfo}); } } }; xhr.onerror = function(){ callback({status:xhr.status, errorMessage:errInfo}); }; } xhr.send(null); }; }
Creator中的资源都会有它的uuid,都会调用该方法进行下载。而uuid资源可能以2种形式存在,第一种是单独的json文件,好比一个prefab或spriteFrame资源,都有本身的json文件。而另外一种则是打包资源,所谓的Pack就是将多个json文件合并为一个json文件,把各个json文件中的json对象组合到一个json数组中,从而达到减小IO的做用。downloadUuid方法会使用PackDownloader进行下载,若是下载失败则使用json的下载方式,也就是downloadText。
function downloadUuid (item, callback) { var result = PackDownloader.load(item, callback); if (result === undefined) { return this.extMap['json'](item, callback); } else if (!!result) { return result; } }
PackDownloader的load方法实现以下,根据uuidToPack中的uuid取出packUuid,若是packUuid不存在,则说明这个uuid没有被打包,直接使用json的方式加载便可。接下来再根据globalUnpackers[packUuid]取出unpacker,调用unpacker.retrieve(uuid)解析出json并返回。
load: function (item, callback) { var uuid = item.uuid; var packUuid = uuidToPack[uuid]; if (!packUuid) { // 返回undefined以让调用者知道它未被识别。 // 不返回false,由于改变返回值类型可能会致使jit失败,尽管返回undefined可能有相同的问题 return; } // 一个uuid有可能被重复打包到多个json文件中,《从编辑器到运行时》一章会介绍这种状况如何产生 if (Array.isArray(packUuid)) { // 这里会遍历多个Pack,从中选择状态最接近加载完成的Pack(谁先加载完用谁)。 packUuid = this._selectLoadedPack(packUuid); } // 取出unpacker,若是加载完成了,从unpacker中取出对应uuid的json对象返回。 var unpacker = globalUnpackers[packUuid]; if (unpacker && unpacker.state === PackState.Loaded) { var json = unpacker.retrieve(uuid); if (json) { return json; } else { return error(uuid, packUuid); } } else { // 其余状况为未加载完成 // unpacker为空则建立一个 if (!unpacker) { if (!CC_TEST) { console.log('Create unpacker %s for %s', packUuid, uuid); } unpacker = globalUnpackers[packUuid] = new JsonUnpacker(); unpacker.state = PackState.Downloading; } // 若是正在加载中或未加载,会走_loadNewPack也就是cc.loader.load,但cc.loader中规避了重复加载。 this._loadNewPack(uuid, packUuid, callback); } // 返回null,让调用者知道它正在异步加载 return null; }
接下来咱们进一步了解一下PackDownloader这个类作了什么?Pack又是什么?globalUnpackers和packIndices又是什么?
PackDownloader作的事情主要是对Json文件的解析、管理和获取。在某些状况下多个json文件会被打包成一个json文件,如AnimationClip文件,在编辑器制做的时候每一个动画都是一个Clip文件(json文件),而在打包以后这些Clip会被合并成一个新的json文件(这样作的目的是节省IO),这就是Pack。
当咱们发布项目时Creator自动帮咱们进行合并,多个json对象组成一个数组对象,packIndices记录了每一个packUuid对应的一组uuid(也就是一个pack文件中合并了哪些文件),每一个文件的uuid对应这个json数组对象的下标。packIndices[packUuid]的下标1是该packUuid对应合并后的json数组下标1这个json对象的uuid。
每一个Clip都有一个uuid,经过uuidToPack的索引获取这个Clip对应的packUuid,也就是合并Json的uuid,这个uuid会对应一个JsonUnpacker,JsonUnpacker会将合并后的json进行解析并缓存,同时保持一个映射,在这里就是每一个Clip的uuid对应的json对象。
// 初始化Packs,这里传入的packs是一个二维数组,首先它是一个uuids的数组,一组uuid被视为一个pack,packs就是一组pack // 每一个uuids都是一个数组,记录了这个pack中合并的全部uuid。 initPacks: function (packs) { packIndices = packs; for (var packUuid in packs) { var uuids = packs[packUuid]; for (var i = 0; i < uuids.length; i++) { var uuid = uuids[i]; // the smallest pack must be at the beginning of the array to download more first // 最小的pack必须放在数组的前面,以便下载更多的包。 var pushFront = uuids.length === 1; // map - uuidToPack, key - uuid, value - packUuid (若是已存在该key,value会添加到数组中) pushToMap(uuidToPack, uuid, packUuid, pushFront); } } }, // 加载一个新的Pack时会调用该方法,根据packUuid去获取url,并当即下载(ignoreMaxConcurrency为true) _loadNewPack: function (uuid, packUuid, callback) { var self = this; var packUrl = cc.AssetLibrary.getLibUrlNoExt(packUuid) + '.json'; cc.loader.load({ url: packUrl, ignoreMaxConcurrency: true }, function (err, packJson) { if (err) { cc.errorID(4916, uuid); return callback(err); } var res = self._doLoadNewPack(uuid, packUuid, packJson); if (res) { callback(null, res); } else { callback(error(uuid, packUuid)); } }); }, // 当一个Pack加载完以后,会回调该方法 _doLoadNewPack: function (uuid, packUuid, packJson) { var unpacker = globalUnpackers[packUuid]; // double check cache after load // 只要unpacker的状态不是PackState.Loaded,进行解析并切换状态 if (unpacker.state !== PackState.Loaded) { unpacker.read(packIndices[packUuid], packJson); unpacker.state = PackState.Loaded; } return unpacker.retrieve(uuid); }, // 遍历多个packUuid,只要找到第一个状态为PackState.Loaded的unpacker // 找不到则找一个最接近PackState.Loaded的unpacker _selectLoadedPack: function (packUuids) { var existsPackState = PackState.Invalid; var existsPackUuid = ''; for (var i = 0; i < packUuids.length; i++) { var packUuid = packUuids[i]; var unpacker = globalUnpackers[packUuid]; if (unpacker) { var state = unpacker.state; if (state === PackState.Loaded) { return packUuid; } else if (state > existsPackState) { existsPackState = state; existsPackUuid = packUuid; } } } return existsPackState !== PackState.Invalid ? existsPackUuid : packUuids[0]; },
JsonUnpacker.prototype.read = function (indices, data) { var jsons = typeof data === 'string' ? JSON.parse(data) : data; if (jsons.length !== indices.length) { cc.errorID(4915); } for (var i = 0; i < indices.length; i++) { var key = indices[i]; var json = jsons[i]; this.jsons[key] = json; } }; JsonUnpacker.prototype.retrieve = function (key) { return this.jsons[key] || null; };
这里传入的data是一个数组json对象,indices是一个uuid数组,read的职责就是将indices[i]做为uuid,对应的jsons[i]做为json对象,记录到this.jsons这个容器中,那么后面的retrieve就能够用uuid来获取对应的json对象了。
01204b0d7.json文件对应的内容在格式化查看工具中打开以下所示,正好是一个拥有5个对象的json数组,第一个对象是Array、后面是4个Object对象。而上图对应的packedAssets下的01204b0d7对象数组为这个json数组的uuid,按下标一一对应。
在原平生台下会执行jsb-loader.js下的内容,对于字体、音效、脚本和图片使用新的下载方法。
// 字体使用了empty function empty (item, callback) { return null; } // 下载脚本直接使用require便可 function downloadScript (item, callback) { require(item.url); return null; } // 声音不须要下载,声音的加载流程包含了下载 function downloadAudio (item, callback) { return item.url; } // 图片分3种状况,textureCache中缓存直接使用、远程图片使用jsb.loadRemoteImg、本地图片使用textureCache的addImageAsync方法加载。 function loadImage (item, callback) { var url = item.url; var cachedTex = cc.textureCache.getTextureForKey(url); if (cachedTex) { return cachedTex; } else if (url.match(jsb.urlRegExp)) { jsb.loadRemoteImg(url, function(succeed, tex) { if (succeed) { tex.url = url; callback && callback(null, tex); } else { callback && callback(new Error('Load image failed: ' + url)); } }); } else { var addImageCallback = function (tex) { if (tex instanceof cc.Texture2D) { tex.url = url; callback && callback(null, tex); } else { callback && callback(new Error('Load image failed: ' + url)); } }; cc.textureCache._addImageAsync(url, addImageCallback); } }
在项目发布时,会根据发布平台生成最终的执行代码。构建原平生台时Creator1.x会指定engine/jsb目录下的脚本,而Creator2.x指定的是engine/bin目录下的jsb脚本。