import Report from "../net/Report"; import SKDataUtil from "./SKDataUtil"; import SKLogger from "./SKLogger"; import SKTimeUtil from "./SKTimeUtil"; /** * 熱更新工具 * SKHotUpdate 1.0.0 */ export default class SKHotUpdate { public static hasNewBlock: () => void; public static changeBlock: (info: string) => void; public static progressBlock: (info: string) => void; public static endBlock: () => void; static shared: SKHotUpdate = new SKHotUpdate(); static isDetails: boolean = false; // dir: string = "remote-asset"; dir: string = "fixed"; manifest: cc.Asset; storagePath: string; assetsManager: jsb.AssetsManager; updating: boolean = false; version: string = ""; remoteVesion: string = ""; checkPercent: string; failCount: number; maxFailCount: number; percent: number = 0; checkTimer: number = 0; // 定時檢查更新 static delayCheck() { this.shared.delayCheck(); } // 檢查熱更新 static checkUpdate() { this.shared.checkHot(); } // 修改熱更新地址 static modify(updateURL: string) { this.shared.modify(updateURL); } // 修改熱更新地址 private modify(updateURL: string) { if (!cc.sys.isNative) { SKLogger.debug("H5不能修改熱更新地址!"); return; } if (this.updating) { SKLogger.debug("正在熱更中,不能修改熱更新地址!"); return; } let manifest = `${this.rootPath()}/project.manifest`; let hasUpdate = jsb.fileUtils.isFileExist(manifest); if (!hasUpdate) { if (this.manifest == null) { cc.loader.loadRes("project", (err, result) => { if (err) { SKLogger.warn(`加載本地清單文件失敗:${err}`); return; } this.manifest = result; this.modifyManifest(this.manifest.nativeUrl, updateURL); }) } else { this.modifyManifest(this.manifest.nativeUrl, updateURL); } } else { this.modifyManifest(manifest, updateURL); } } // 修改project.manifest文件 private modifyManifest(manifestPath: string, updateURL: string) { let loadManifest = jsb.fileUtils.getStringFromFile(manifestPath); let data = SKDataUtil.jsonBy(loadManifest); if (data.packageUrl == updateURL) { SKLogger.debug(`熱更清單[${manifestPath}]包地址已經是[${updateURL}]!`); return; } let lastURL = data.packageUrl; data.packageUrl = updateURL; data.remoteManifestUrl = `${updateURL}/project.manifest`; data.remoteVersionUrl = `${updateURL}/version.manifest`; let afterString = SKDataUtil.toJson(data); let isWritten = jsb.fileUtils.writeStringToFile(afterString, manifestPath); if (isWritten) { SKLogger.debug(`熱更清單[${manifestPath}]包地址[${lastURL}->${updateURL}]修改成功`); } else { SKLogger.debug(`熱更清單[${manifestPath}]包地址[${lastURL}->${updateURL}]修改失敗`); } } // 結束 private end() { if (!this.updating) { return; } this.updating = false; if (this.assetsManager) { this.assetsManager.setEventCallback(null); } if (SKHotUpdate.hasNewBlock) { let self = this; this.checkTimer = SKTimeUtil.delay(() => { self.checkHot(); }, 60 * 5); return; } if (SKHotUpdate.endBlock) { SKHotUpdate.endBlock(); } } // 檢查更新 private checkHot() { if (!cc.sys.isNative) { SKLogger.debug("H5無需檢查熱更新!"); if (SKHotUpdate.endBlock) { SKHotUpdate.endBlock(); } return; } SKLogger.debug("開始檢查更新..."); if (this.manifest == null) { cc.loader.loadRes("project", (err, result) => { if (err) { SKLogger.warn(`加載本地清單文件失敗:${err}`); this.end(); return; } this.manifest = result; this.checkUpdate(); }) } else { this.checkUpdate(); } } rootPath(): string { let result = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + this.dir); if (cc.sys.os === cc.sys.OS_IOS) { result = result + "Documents/"; } return result; } // 檢查更新 checkUpdate() { if (this.updating) { SKLogger.debug("正在檢查更新..."); return; } if (!this.assetsManager) { this.storagePath = this.rootPath(); SKLogger.debug(`檢查本地緩存,存儲路徑:${this.storagePath}`); this.assetsManager = new jsb.AssetsManager("", this.storagePath, this.compareVersion.bind(this)); this.assetsManager.setVerifyCallback((path: string, asset: any) => { let compressed = asset.compressed; let expectedMD5 = asset.md5; let relativePath = asset.path; let size = asset.size; SKLogger.debug(`版本驗證:是否壓縮=${compressed},MD5=${expectedMD5},相對路徑=${relativePath},大小=${size}`); if (compressed) { return true; } else { return true; } }); if (cc.sys.os === cc.sys.OS_ANDROID) { // @ts-ignore this.assetsManager.setMaxConcurrentTask(5); } } // 加載本地manifest if (this.assetsManager.getState() === jsb.AssetsManager.State.UNINITED) { let url = this.manifest.nativeUrl; SKLogger.debug(`加載本地清單:${url}`); this.assetsManager.loadLocalManifest(url); } let localManifest = this.assetsManager.getLocalManifest(); if (!this.assetsManager.getLocalManifest() || !localManifest.isLoaded()) { SKLogger.warn("加載本地清單失敗!"); this.end(); return; } this.version = localManifest.getVersion(); SKLogger.debug(`本地版本:${this.version}`); Report.version = this.version; this.assetsManager.setEventCallback(this.checkEvent.bind(this)); this.assetsManager.checkUpdate(); this.updating = true; } // 檢查事件 private checkEvent(event: jsb.EventAssetsManager) { let code = event.getEventCode(); let msg = event.getMessage(); let hasUpdate: boolean = false; switch (code) { case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: SKLogger.warn(`檢查:未找到本地清單文件[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: SKLogger.warn(`檢查:下載清單文件錯誤[code=${code},msg=${msg}]`) this.end(); case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: SKLogger.debug(`檢查:解析清單文件錯誤[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: SKLogger.debug(`檢查:已經是最新的版本:${this.version}[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.UPDATE_PROGRESSION: SKLogger.debug(`檢查:正在進行中...[code=${code},msg=${msg}]`); return; case jsb.EventAssetsManager.NEW_VERSION_FOUND: let info = `檢查:有新版本:${this.version}->${this.remoteVesion}`; // @ts-ignore let size = (this.assetsManager.getTotalBytes() / 1024 / 1024).toFixed(1); info += ` ${size}KB`; SKLogger.debug(info); hasUpdate = true; break; case jsb.EventAssetsManager.ERROR_UPDATING: SKLogger.warn(`檢查:錯誤的更新...`); break; case jsb.EventAssetsManager.UPDATE_FINISHED: SKLogger.debug(`檢查:更新完成`); this.end(); break; default: SKLogger.warn(`檢查:未處理的檢查事件[code=${code},msg=${msg}]`); return; } this.assetsManager.setEventCallback(null); this.updating = false; if (hasUpdate) { let info = `檢查:有新版本:${this.version}->${this.remoteVesion}`; // @ts-ignore let size = Math.ceil(this.assetsManager.getTotalBytes() / 1024); info += ` ${size}KB`; if (SKHotUpdate.hasNewBlock) { SKHotUpdate.hasNewBlock(); return; } if (SKHotUpdate.changeBlock) { SKHotUpdate.changeBlock(info); } this.startUpdate(); } } // 更新事件 private updateEvent(event: jsb.EventAssetsManager) { let code = event.getEventCode(); let msg = event.getMessage(); let assetId = event.getAssetId(); if (!assetId) { assetId = ""; } switch (code) { case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: SKLogger.debug(`更新:沒有本地清單[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: SKLogger.debug(`更新:下載清單失敗[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: SKLogger.debug(`更新解析清單失敗[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.NEW_VERSION_FOUND: SKLogger.debug(`更新:有新的版本[${this.remoteVesion}]`); break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: SKLogger.debug(`更新:已經是最新版本[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.UPDATE_PROGRESSION: let percent = event.getPercent(); if (isNaN(percent)) { if (this.checkPercent.length < 3) { this.checkPercent += "."; } else { this.checkPercent = ""; } let show = this.checkPercent; for (let i = this.checkPercent.length; i < 3; i++) { show += " "; } SKLogger.debug(`更新:進行中[code=${code},msg=${msg}]`); this.percent = 0; if (SKHotUpdate.progressBlock) { let info = `更新:正在進行中${show}`; SKHotUpdate.progressBlock(info); } } else { this.percent = percent * 100; let result = (Math.min(1, percent) * 100).toFixed(2) + "%"; let loadedFiles = event.getDownloadedFiles(); let totalFiles = event.getTotalFiles(); let loadedBytes = (event.getDownloadedBytes() / 1024 / 1024).toFixed(1); let totalBytes = (event.getTotalBytes() / 1024 / 1024).toFixed(1); let info = `更新:進度...${result}`; if (SKHotUpdate.isDetails) { info += `-文件(${loadedFiles}/${totalFiles})大小(${loadedBytes}MB/${totalBytes}MB)`; } SKLogger.debug(`${info}:${msg}`); if (SKHotUpdate.progressBlock) { SKHotUpdate.progressBlock(info); } } break; case jsb.EventAssetsManager.ASSET_UPDATED: SKLogger.debug(`更新:ASSET更新[assetId=${assetId}]`); break; case jsb.EventAssetsManager.UPDATE_FINISHED: SKLogger.debug(`更新:完成[code=${code},msg=${msg}]`); this.restart(); break; case jsb.EventAssetsManager.ERROR_UPDATING: SKLogger.debug(`更新:錯誤[code=${code},msg=${msg}]`); this.end(); break; case jsb.EventAssetsManager.UPDATE_FAILED: SKLogger.debug(`更新:失敗[assetId=${assetId}]`); this.failCount++; if (this.failCount < this.maxFailCount) { this.assetsManager.downloadFailedAssets(); let info = `更新失敗重試第${this.failCount}次...`; if (SKHotUpdate.progressBlock) { SKHotUpdate.progressBlock(info); } } else { this.failCount = 0; this.end(); } break; case jsb.EventAssetsManager.ERROR_DECOMPRESS: SKLogger.debug(`更新:解壓錯誤[code=${code},msg=${msg}]`); this.end(); break; default: SKLogger.debug(`更新:未處理的更新事件[code=${code},msg=${msg}]`); this.end(); break; } } // 重啟 private restart() { SKLogger.debug("更新完成,重啟遊戲..."); this.version = this.remoteVesion; Report.version = this.version; this.assetsManager.setEventCallback(null); // @ts-ignore let searchPaths = jsb.fileUtils.getSearchPaths(); let newPaths = this.assetsManager.getLocalManifest().getSearchPaths(); Array.prototype.unshift(searchPaths, newPaths); cc.sys.localStorage.setItem("HotUpdateSearchPaths", JSON.stringify(searchPaths)); jsb.fileUtils.setSearchPaths(searchPaths); cc.audioEngine.stopAll(); cc.game.restart(); } // 版本比較 private compareVersion(versionA: string, versionB: string): number { SKLogger.debug(`版本比較:${versionA}->${versionB}`); this.remoteVesion = versionB; let vA = versionA.split('.'); let vB = versionB.split('.'); for (let i = 0; i < vA.length; ++i) { let a = parseInt(vA[i]); let b = parseInt(vB[i] || '0'); if (a === b) { continue; } else { return a - b; } } if (vB.length > vA.length) { return -1; } return 0; } // 開始更新 startUpdate() { if (this.updating) { SKLogger.debug(`正在更新中...`); return; } if (this.assetsManager == null) { SKLogger.debug("沒有下載管理器,無法更新!"); this.end(); return; } this.assetsManager.setEventCallback(this.updateEvent.bind(this)); // 加載本地manifest if (this.assetsManager.getState() === jsb.AssetsManager.State.UNINITED) { let url = this.manifest.nativeUrl; if (cc.loader.md5Pipe) { url = cc.loader.md5Pipe.transfromURL(url); } this.assetsManager.loadLocalManifest(url); } this.assetsManager.update(); this.updating = true; this.checkPercent = ""; this.failCount = 0; this.maxFailCount = 3; let info = "開始更新..."; SKLogger.debug(info); if (SKHotUpdate.changeBlock) { SKHotUpdate.changeBlock(info); } } delayCheck(): void { if (!cc.sys.isNative) { SKLogger.debug("H5無需檢查熱更新!"); if (SKHotUpdate.endBlock) { SKHotUpdate.endBlock(); } return; } if (this.updating) { SKLogger.debug("正在更新中,不能重複檢查!"); return; } let self = this; cc.game.on(cc.game.EVENT_HIDE, function () { SKLogger.debug("熱更:遊戲進入後台"); self.checkTimer = SKTimeUtil.cancelLoop(self.checkTimer); self.checkHot(); }, this); cc.game.on(cc.game.EVENT_SHOW, function () { SKLogger.debug("熱更:重新返回游戲"); self.checkTimer = SKTimeUtil.cancelLoop(self.checkTimer); self.checkHot(); }, this); this.checkTimer = SKTimeUtil.delay(() => { self.checkHot(); }, 60 * 5); } }