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腳本。