444 lines
16 KiB
TypeScript
444 lines
16 KiB
TypeScript
|
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);
|
||
|
}
|
||
|
}
|