Cocos Creator3.8.8 热更新成功,仍是旧的预制体和旧的脚本

尝试根据文档实现安卓包热更新功能
步骤:
1:本地和远程manifest 版本比对正常
2:最新版本成功更新
3:更新后重启成功
问题
重启后,新版本得预制体和脚本逻辑仍是旧的
如果手动清除缓存,或者卸载重装,热更新后预制体和逻辑都是最新版本的内容

排查步骤
1:是否下载远程资源到本地内
通过android studio 看应用的私有目录,比对更新时间,和内容,确定已经下载了新版本资源
2:搜索路径优先级是否是热更目录优先
通过打印看到路径确实是热更新目录是最新的
还有别的排查选项吗?
我没辙了…
枯了…
救救孩子吧呜呜呜呜,为什么啊?
有没有义父给我指条路…
还有别的方法吗…

勾选了MD5?

打印一下使用的uuid

没有勾选,教程写了注意不要勾选,所以没勾MD5

有設定hotUpdateSearchPaths嗎?

运行时prefab.uuid:69cc0c1e-6125-47e7-8855-1525afec9f0f
cofig.json 显示的是这样的,05680aa4f,uuids 第27个"69zAweYSVH54hVFSWv7J8P",是这个…对不上,我找错地方了吗?是不是我的认知不足还是啥…
关于UUID 排查步骤
1、对比修改了预制体的版本1和版本2 的config.json 发现,并没有差异和变化
2、对比未更新前的预制体的uuid打印字段,卸载后重装的预制体uuid 也没有变化,一直都是69cc0c1e-6125-47e7-8855-1525afec9f0f,实际上两个界面显示是有变化的
按理说不应该会不一样吗,

我在监听热更新完成,重启前,设置了
hotUpdateSearchPaths
private applyHotUpdateSearchPaths(): void {

    if (!this._am) return;

    const manifest = this._am.getLocalManifest();

    if (!manifest || !manifest.isLoaded()) return;

    const hotPaths = manifest.getSearchPaths();   // ← 这就是热更目录数组,例如 [".../dasi_hotupdate/"]

    if (!hotPaths || hotPaths.length === 0) return;

    const sysPaths = this.jsb.fileUtils.getSearchPaths();

    // 去重:避免多次 unshift 导致同一路径出现 N 次

    const already = hotPaths.every(p => sysPaths.includes(p));

    if (!already) {

        Array.prototype.unshift.apply(sysPaths, hotPaths);

    }

    this.jsb.fileUtils.setSearchPaths(sysPaths);

    sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(sysPaths));

    Logger.log('【SearchPaths】已应用热更搜索路径:', sysPaths);

   

}

import { _decorator, Component, Node, director, sys, Asset, game, instantiate, AssetManager, assetManager } from ‘cc’;

import { ResourceLoader } from ‘./ResourceLoader’;

import { Logger } from ‘./Logger’;

import { HotUpdatePage } from ‘./HotUpdatePage’;

const { ccclass, property } = _decorator;

@ccclass(‘LaunchLoading’)

export class LaunchLoading extends Component {

@property(Node)

public popRoot: Node = null!;

@property(Asset)

public manifestUrl: Asset = null!;

private _updatePanel: HotUpdatePage | null = null;

private jsb: any = null;

private _am: any = null;

private _storagePath = '';

private _updating: boolean = false;

private _canRetry: boolean = false;

// 新增:缓存热更新信息,用于弹窗展示

private _updateTotalFiles: number = 0;   // 待更新总文件数

private _updateTotalBytes: number = 0;   // 待更新总字节数

private _newVersion: string = "";        // 远端新版本号

onLoad() {

    Logger.log("【入口】LaunchLoading onLoad执行");

    this.initHotUpdateEnv();

}

private initHotUpdateEnv() {

    // ==========新增平台调试打印==========

    Logger.log("【平台检测】sys.isNative =", sys.isNative);

    Logger.log("【平台检测】sys.platform =", sys.platform);

    Logger.log("【平台检测】原始window.jsb是否存在 =", !!(window as any).jsb);

    this.jsb = (window as any).jsb;

    Logger.log("【JSB赋值后】this.jsb是否有效 =", !!this.jsb);

    //============================一直都是旧包除非重启所以增加打印=======================================

    if (this.jsb) {

        const paths = this.jsb.fileUtils.getSearchPaths();

        Logger.log("【路径-启动初始化】当前fileUtils搜索路径:", JSON.stringify(paths));

    }

    //==============================================================

    // 拆分判断,精准打印是哪个条件触发跳过

    if (!this.jsb) {

        Logger.error("【跳过原因】window.jsb不存在,非原生环境,跳过热更");

        this.enterGameDirect();

        return;

    }

    if (!sys.isNative) {

        Logger.error("【跳过原因】sys.isNative=false,非原生环境,跳过热更");

        this.enterGameDirect();

        return;

    }

    // ======================================

    this._storagePath = this.jsb.fileUtils.getWritablePath() + "dasi_hotupdate/";

    Logger.log("【路径】热更本地缓存路径:", this._storagePath);

    const versionCompare = (vA: string, vB: string) => {

        Logger.log("【版本对比】本地ver:", vA, "远端ver:", vB);

        const arrA = vA.split(".");

        const arrB = vB.split(".");

        for (let i = 0; i < arrA.length; i++) {

            const numA = Number(arrA[i]);

            const numB = Number(arrB[i] || "0");

            if (numA !== numB) return numA - numB;

        }

        return arrB.length > arrA.length ? -1 : 0;

    };

    const localManifestPath = this.manifestUrl.nativeUrl;

    Logger.log("【AM初始化】传入本地manifest物理路径:", localManifestPath);

    this._am = new this.jsb.AssetsManager(localManifestPath, this._storagePath, versionCompare);

    // this._am = new this.jsb.AssetsManager("", this._storagePath, versionCompare);

    Logger.log("【AM初始化】AssetsManager实例创建完成");

    this._am.setVerifyCallback((path: string, assetInfo: any) => {

        const isZip = assetInfo.compressed;

        const md5 = assetInfo.md5;

        const relPath = assetInfo.path;

        if (isZip) {

            Logger.log("【校验】压缩包校验通过:", relPath);

            return true;

        } else {

            Logger.log("【校验】文件校验通过:", relPath, md5);

            return true;

        }

    });

    Logger.log("【流程】准备进入checkVersion检测版本");

    this.checkVersion();

}

/** 让 FileUtils 立刻感知热更目录(幂等操作,调多次不伤) */

private applyHotUpdateSearchPaths(): void {

    if (!this._am) return;

    const manifest = this._am.getLocalManifest();

    if (!manifest || !manifest.isLoaded()) return;

    const hotPaths = manifest.getSearchPaths();   // ← 这就是热更目录数组,例如 [".../dasi_hotupdate/"]

    if (!hotPaths || hotPaths.length === 0) return;

    const sysPaths = this.jsb.fileUtils.getSearchPaths();

    // 去重:避免多次 unshift 导致同一路径出现 N 次

    const already = hotPaths.every(p => sysPaths.includes(p));

    if (!already) {

        Array.prototype.unshift.apply(sysPaths, hotPaths);

    }

    this.jsb.fileUtils.setSearchPaths(sysPaths);

    sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(sysPaths));

    Logger.log('【SearchPaths】已应用热更搜索路径:', sysPaths);

   

}

private checkVersion() {

    Logger.log("【检测】进入checkVersion,_updating=", this._updating);

    if (this._updating) {

        Logger.log("【检测】正在检测/更新中,请勿重复触发");

        return;

    }

    const amState = this._am.getState();

    Logger.log("【AM状态】当前AssetsManager状态码:", amState, "UNINITED=", this.jsb.AssetsManager.State.UNINITED);

    const localManifest = this._am.getLocalManifest();

  

    Logger.log("【本地清单】getLocalManifest获取对象是否存在:", !!localManifest);

    let manifestLoaded = false;

    let localVer = "";

    if (localManifest) {

        manifestLoaded = localManifest.isLoaded();

        localVer = localManifest.getVersion(); // 官方唯一合法读取接口

        Logger.log("【本地清单】isLoaded=", manifestLoaded, "本地版本号=", localVer);

    }

    if (!localManifest || !manifestLoaded) {

        Logger.error("【致命】本地manifest加载失败,直接跳过热更进游戏");

        this.enterGameDirect();

        return;

    }

    Logger.log("【检测】绑定更新回调,发起checkUpdate请求远端");

    this._am.setEventCallback(this.checkCb.bind(this));

    this._am.checkUpdate();

    this._updating = true;

    Logger.log("【检测】已上锁_updating=true");

}

private checkCb(event: any) {

    const code = event.getEventCode();

    const errMsg = event.getMessage ? event.getMessage() : "无错误详情";

    Logger.log("【检测回调】事件code=", code, "原生错误msg:", errMsg);

    let isFinalEvent = false;

    let needEnterGame = true;

    switch (code) {

        case this.jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:

            Logger.error("【异常】ERROR_NO_LOCAL_MANIFEST:未找到本地manifest,跳过热更", errMsg);

            isFinalEvent = true;

            break;

        case this.jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:

            Logger.error("【异常】ERROR_DOWNLOAD_MANIFEST:远程manifest下载失败", errMsg);

            isFinalEvent = true;

            break;

        case this.jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:

            Logger.error("【异常】ERROR_PARSE_MANIFEST:远程manifest解析失败【核心报错】", errMsg);

            isFinalEvent = true;

            break;

        case this.jsb.EventAssetsManager.ALREADY_UP_TO_DATE:

            Logger.log("【版本】已是最新版本,直接进游戏");

            isFinalEvent = true;

            this.applyHotUpdateSearchPaths();

            break;

        case this.jsb.EventAssetsManager.NEW_VERSION_FOUND:

            Logger.log("【版本】检测到新版本,准备加载更新弹窗");

            const self = this;

                        // 全部逻辑抛到主线程执行

            // 主线程:通过 AssetsManager 实例读取更新数据(原生环境安全)

            const remoteManifest = self._am.getRemoteManifest();

            const localManifest = self._am.getLocalManifest();

            // 赋值 + 容错兜底(防止空对象报错)

            self._updateTotalFiles = self._am.getTotalFiles() || 0;

            self._updateTotalBytes = self._am.getTotalBytes() || 0;

            self._newVersion = remoteManifest ? remoteManifest.getVersion() : "";

            // 主线程打印日志

            Logger.log(`【更新信息】待更新文件数:${self._updateTotalFiles}`);

            Logger.log(`【更新信息】原始总字节:${self._updateTotalBytes} B`);

            Logger.log(`【更新信息】远端版本:${self._newVersion}`);

            // 加载更新弹窗

            self.loadUpdatePopup();

         

            isFinalEvent = true;

            needEnterGame = false; // 找到新版本,不自动进游戏

            break;

        case this.jsb.EventAssetsManager.UPDATE_PROGRESSION:

            Logger.log("【检测-下载进度】正在拉取远端version.manifest");

            // 进度事件:非结束事件,不做收尾逻辑

            return;

        default:

            Logger.log("【检测】未知事件码:", code);

            isFinalEvent = true;

            break;

    }

    // 仅最终事件才解绑回调、解锁

    if (isFinalEvent) {

        this._am.setEventCallback(null!);

        this._updating = false;

        Logger.log("【检测回调】清空回调,解锁_updating=false");

        if (needEnterGame) {

            Logger.log("【流程】无新版本/异常,进入enterGameDirect加载Bundle");

            this.enterGameDirect();

        }

    }

}

/**

 * 字节格式化:B → KB / MB

 * @param bytes 原始字节数

 * @returns 格式化后带单位的字符串

 */

private formatBytes(bytes: number): string {

    if (bytes < 1024) {

        return `${bytes} B`;

    } else if (bytes < 1024 * 1024) {

        const kb = (bytes / 1024).toFixed(1);

        return `${kb} KB`;

    } else {

        const mb = (bytes / (1024 * 1024)).toFixed(1);

        return `${mb} MB`;

    }

}

private startUpdate() {

    if (this._updating) return;

    this._updating = true;

    this._canRetry = false;

    this._am.setEventCallback(this.updateCb.bind(this));

    Logger.log("【开始下载】调用am.update()");

    this._am.update();

}

private updateCb(event: any) {

    const code = event.getEventCode();

    let needRestart = false;

    let updateFailed = false;

    const msg = event.getMessage ? event.getMessage() : '';

    switch (code) {

        case this.jsb.EventAssetsManager.UPDATE_PROGRESSION:

            const rawPercent = event.getPercent();

            const currFile = event.getDownloadedFiles();

            const totalFile = event.getTotalFiles();

            // 容错:过滤首帧 NaN,无有效进度时显示 0

            let showPercent = 0;

            if (!isNaN(rawPercent)) {

                showPercent = Math.floor(rawPercent * 100); // 转为 0~100 整数百分比

            }

            // 打印日志(不再出现 NaN)

            Logger.log(`【下载进度】文件${currFile}/${totalFile} 进度 ${showPercent}%`);

            if (this._updatePanel) {

                this._updatePanel.setByteProgress(event.getDownloadedBytes(), event.getTotalBytes());

                this._updatePanel.setFileProgress(currFile, totalFile);

                this._updatePanel.setInfo(`正在下载:${msg}`);

            }

            break;

        case this.jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:

        case this.jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:

        case this.jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:

            Logger.error("【更新阶段】清单异常终止更新:", msg);

            updateFailed = true;

            if (this._updatePanel) this._updatePanel.setInfo("更新配置文件拉取失败,跳过更新");

            break;

        case this.jsb.EventAssetsManager.UPDATE_FINISHED:

            this.applyHotUpdateSearchPaths();

            Logger.log("【更新完成】热更全部下载完毕,准备重启");

            needRestart = true;

            if (this._updatePanel) this._updatePanel.setInfo("更新完成,即将重启游戏");

            break;

        case this.jsb.EventAssetsManager.UPDATE_FAILED:

            Logger.error("【更新失败】部分资源下载失败,可重试", msg);

            this._canRetry = true;

            this._updating = false;

            if (this._updatePanel) {

                this._updatePanel.showRetryBtn(this.retryUpdate);

                this._updatePanel.setInfo("部分资源下载失败,请点击重试");

            }

            break;

        case this.jsb.EventAssetsManager.ERROR_UPDATING:

            Logger.error("【资源异常】", event.getAssetId(), msg);

            if (this._updatePanel) {

                this._updatePanel.setInfo(`资源异常:${event.getAssetId()}`);

                this._updatePanel.showRetryBtn(this.retryUpdate);

            }

            break;

        case this.jsb.EventAssetsManager.ERROR_DECOMPRESS:

            Logger.error("【解压失败】", msg);

            if (this._updatePanel) {

                this._updatePanel.setInfo("压缩包解压失败,可重试");

                this._updatePanel.showRetryBtn(this.retryUpdate);

            }

            break;

        case this.jsb.EventAssetsManager.ALREADY_UP_TO_DATE:

            Logger.log("【更新】已是最新资源");

            updateFailed = true;

            if (this._updatePanel) this._updatePanel.setInfo("当前已是最新版本");

            break;

    }

    if (updateFailed) {

        this._am.setEventCallback(null!);

        this._updating = false;

        Logger.log("【更新失败分支】跳转enterGameDirect");

        this.enterGameDirect();

    }

    if (needRestart) {

        this._am.setEventCallback(null!);

        this.applyHotUpdateSearchPaths(); // ← 替换掉原来那段手写的

        // ============================新增日志

        const pathsBeforeRestart = this.jsb.fileUtils.getSearchPaths();

        Logger.log("【路径-更新完成、重启前】", JSON.stringify(pathsBeforeRestart));

        //===================================================================

        Logger.log("【重启】热更路径写入完成,延时重启");

        setTimeout(() => {

            game.restart();

             Logger.log("【重启】调用重启成功?");

        }, 800);

    }

}

public retryUpdate() {

    if (!this._updating && this._canRetry) {

        this._canRetry = false;

        this._updating = true;

        this._am.setEventCallback(this.updateCb.bind(this));

        Logger.log("【重试】开始下载失败资源");

        this._am.downloadFailedAssets();

    }

}

private async loadUpdatePopup() {

    Logger.log("【弹窗】开始从common加载热更弹窗");

    try {

        const popPrefab = await ResourceLoader.Instance.loadPrefabFromCommon("ui/HotUpdatePage");

        const popNode = instantiate(popPrefab);

        this._updatePanel = popNode.getComponent(HotUpdatePage);

        popNode.setParent(this.popRoot);

        Logger.log("【弹窗】正常加载common热更弹窗");

        // ========= 新增:向主弹窗传递更新数据 =========

        if (this._updatePanel) {

            const sizeStr = this.formatBytes(this._updateTotalBytes);

            // 调用弹窗方法 / 直接赋值属性(二选一,推荐用方法)

            this._updatePanel.setUpdateInfo(

                this._newVersion,

                this._updateTotalFiles,

                sizeStr

            );

            this._updatePanel.setByteProgress(0,this._updateTotalBytes);

            this._updatePanel.setFileProgress(0,  this._updateTotalFiles);

            this._updatePanel.showSureBtn(this.startUpdate.bind(this));

            Logger.log(">>>>>>>>>>>>>显示确定弹窗");

        } else {

            Logger.log(">>>>>>>>>>>>>>>>>hotupdate 脚本为空");

        }

    } catch (err) {

        Logger.error("【弹窗异常】common弹窗加载失败,启用本地兜底弹窗", err);

        const backupPrefab = await ResourceLoader.loadPrefab("ui/HotUpdateBackupPage");

        if (backupPrefab) {

            const popNode = instantiate(backupPrefab);

            this._updatePanel = popNode.getComponent(HotUpdatePage);

            popNode.setParent(this.popRoot);

            Logger.log("【弹窗】兜底弹窗加载成功");

            // ========= 新增:向兜底弹窗也传递更新数据 =========

            if (this._updatePanel) {

                const sizeStr = this.formatBytes(this._updateTotalBytes);

                this._updatePanel.setUpdateInfo(

                    this._newVersion,

                    this._updateTotalFiles,

                    sizeStr

                );

                this._updatePanel.setByteProgress(0,this._updateTotalBytes);

                this._updatePanel.setFileProgress(0,  this._updateTotalFiles);

                this._updatePanel.showSureBtn(this.startUpdate.bind(this));

                Logger.log(">>>>>>>>>>>>>显示确定弹窗");

            } else {

                Logger.log(">>>>>>>>>>>>>>>>>hotupdate 脚本为空");

            }

        } else {

            Logger.error("【弹窗】兜底弹窗也加载失败");

        }

    }

}

private async enterGameDirect() {

    // 路径日志(上一层已加,保留)

    if (this.jsb) {

        const finalPaths = this.jsb.fileUtils.getSearchPaths();

        Logger.log("【路径-加载common前最终快照】", JSON.stringify(finalPaths));

    }

    // ========== 新增:检查常驻Bundle缓存状态 ==========

    const hasCommonCache = ResourceLoader.Instance.checkResidentBundle("common");

    Logger.log("【Bundle缓存-加载前】common是否存在内存缓存:", hasCommonCache);

    // ==============================================

    Logger.log(">>>>>>>>>>>>>>>>>>>>>>先加载common")

    await ResourceLoader.Instance.loadResidentBundle("common");

    // ========== 新增:加载后再打印 ==========

    Logger.log("【Bundle缓存-加载后】common加载完成");

    // ==============================================

    Logger.log(">>>>>>>>>>>>>>>>>>>>>>common加载完毕,再加载remote_scripts")

    await ResourceLoader.Instance.loadResidentBundle("remote_scripts");

    Logger.log(">>>>>>>>>>>>>>>>>>>>>>两个Bundle加载完成,跳转test场景")

    director.loadScene("test");

}

}这是我的热更新脚本,我给豆老师看过了,说没有问题,没有问题就是最大的问题,我猜测还可能是我的resouceloade 脚本写错了,或者缓存问题?可是缓存在重启,或者杀死进程就应该没有了啊,为啥还是旧的呢,这很奇怪,这太奇怪了…

main.js中添加搜索路径没有

https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/hot-update.html

我說的是構建後的main.js
// 在 main.js 的开头添加如下代码
(function () {
if (typeof window.jsb === ‘object’) {
var hotUpdateSearchPaths = localStorage.getItem(‘HotUpdateSearchPaths’);
if (hotUpdateSearchPaths) {
var paths = JSON.parse(hotUpdateSearchPaths);
jsb.fileUtils.setSearchPaths(paths);

        var fileList = [];
        var storagePath = paths[0] || '';
        var tempPath = storagePath + '_temp/';
        var baseOffset = tempPath.length;

        if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
            jsb.fileUtils.listFilesRecursively(tempPath, fileList);
            fileList.forEach(srcPath => {
                var relativePath = srcPath.substr(baseOffset);
                var dstPath = storagePath + relativePath;

                if (srcPath[srcPath.length] == '/') {
                    jsb.fileUtils.createDirectory(dstPath)
                }
                else {
                    if (jsb.fileUtils.isFileExist(dstPath)) {
                        jsb.fileUtils.removeFile(dstPath)
                    }
                    jsb.fileUtils.renameFile(srcPath, dstPath);
                }
            })
            jsb.fileUtils.removeDirectory(tempPath);
        }
    }
}

})();

可以考虑试下我的插件

https://www.yuque.com/dhunterstudio/gg/hot-update

https://store.cocos.com/app/detail/6756

这段代码只要用了version_generator.js 就会自动加上的,我检查构建后的main.js 也是有这段的代码的

会不会是查找目录的第一个不是热更的目录

我打印出来热更新目录就是第一个查找的目录,我用官方教程里面的案例工程,结果也是一样的,热更新重启后仍是上一个版本的预制体。我的步骤就是
1、把官方案例下载,并用3.8.8的cocos creator 打开,自己手动创建一个common/ui/test.prefab
2、把common 设置为远程bundle包
3、修改hotupdat 代码,版本一致,后实例化test.prefab
4、关闭扩展插件hotupdate_local ip
5、设置远程服务器url,构建发布android
6、用version_geneator.js 打本地manifest
7、把第6步生成的manifest 复制到项目工程.assets/下
8、重新构建发布android
9、把生成的两个manifest复制到remote下,把构建的remote 整个复制到远程服务器,这就是初版1.0.0
10、运行第一个包,因为版本一致直接实例化test.prefab
11、修改test.prefab ,命令行加一个版本1.0.1重新运行version_generator.js,生成新版本manifest
12、把第11步生成的两个manifest复制到remote下,把新版本的remote 整个复制到远程服务器
13、真机运行
14、检测更新—>下载更新---->重启---->实例化test.prefab
一直都是第一版的test.prefab…只有卸载重装,再热更,才会是最新版,直接用官方的都是这样,是我步骤错了吗,我有一点死了

加了的,使用version_generator.js 会自动再main.js 加添加搜索路径的代码

(帖子被作者删除,如无标记将在 24 小时后自动删除)


这一步有点问题 你应该是在remote , 根据后缀 找到对应文件 替换 两个manifest 保持名字和remote找到文件名字一样就行了

我建议你用下载一个安卓模拟器,设置root,比对一下可写目录下和热更新目录的变化的文件,拿出来比对一下,肯定是那里有问题了

问题大概率就是按个mianfest 文件

老师,我有一点看不懂
“这一步有点问题 你应该是在remote , 根据后缀 找到对应文件 替换 两个manifest 保持名字和remote找到文件名字一样就行了”
,我上面的步骤中,remote/ 是构建发布自动生成的资源文件夹,我把这整个文件夹复制到服务器,并把命令行生成的新版本的Manifest 直接复制到服务器的remote/下,然后打开APP测试热更新,实际他确实读到了需要更新的文件的文件数量和大小,根据这个现象,我觉得热更新应该是成功的。老师可以再解释下吗