
做 Unity 转微信小游戏的迭代最恶心的不是打包流程不是分包策略而是改完东西发出去玩家打开还是老资源。你明明在后台改了 BUG用户那边却还跑着旧逻辑很多人第一反应是“微信缓存了”其实背后是 Unity WebGL 产物和微信小游戏文件缓存叠加在一起的结果。这篇文章我就把这次版本迭代踩的坑、查的原理和最终落地的缓存过期方案完整拆一遍希望能帮正在做小游戏迭代的你少走弯路。我做的项目是 Unity 2021 导出的微信小游戏首次包体拆了首包和远程资源包热更资源走 CDN整套玩法用的是 Addressables。前期开发阶段没人在意缓存问题测试的时候 Android 和 iOS 都正常但版本一迭代微信小游戏端就出问题了重复下载、资源不更新、旧版本资源混在新版本逻辑里跑最头疼的是 UI 和美术资源错乱但代码逻辑却没跟上。1. 先搞清楚微信小游戏到底在缓存什么1.1 微信小游戏的双层缓存机制很多 Unity 开发者想当然地以为微信小游戏就是把 WebGL 产物塞进一个容器里加载行为跟浏览器差不多。实际上微信小游戏运行时有自己的文件系统能力它不是标准的浏览器环境没有 JS 的持久存储 API而是提供wx.getFileSystemManager()这样的接口去操作本地文件。Unity 的 WebGL 构建产物在微信小游戏环境里会被包成“微信小游戏包”然后资源分两种方式加载一种是随包体直接内置另一种是运行时通过网络请求加载。版本迭代时出问题的多半是“运行时通过网络请求加载”的部分。Unity 的 WebGL 加载器默认会把远程资源文件当作普通 HTTP 资源去拉取而微信小游戏环境的网络请求会走微信的缓存逻辑同时 Unity 侧的indexedDB缓存机制会被适配层改写成小游戏文件系统缓存。于是形成了双层缓存第一层是微信小游戏运行时自带的资源缓存第二层是 Unity 加载器自己维护的本地文件缓存。这里要特别强调一个关键点微信小游戏的缓存不是简单的“浏览器 304 缓存”它是持久化的本地文件系统。也就是说即使你把 CDN 上的资源文件删掉玩家设备上依然可能留着旧版本的.bundle、.json、.png文件下次启动时 Unity 适配层会优先去本地找这些文件找不到才会发起网络请求。这就导致你这个版本如果改动了资源路径或资源内容没有配合缓存失效策略玩家就会一直被旧资源“牵着走”。1.2 Unity WebGL 构建产物中有哪些文件会影响缓存一次完整的 Unity 微信小游戏构建会产出一堆文件。最常见的有project.config.json、game.js、game.json、webgl.wasm.framework.unityweb、webgl.wasm.code.unityweb、webgl.data.unityweb老版本叫.data以及一堆资源分包的.bundle文件。如果你用的是 Unity 2020 以上版本还有可能看到webgl.wasm.symbols.unityweb这个文件在调试时才有正式发布基本可以忽略。真正影响版本迭代的是webgl.data.unityweb和各类.bundle文件。webgl.data.unityweb打包了场景、材质、shader 等基础资源这些资源如果改过整个文件哈希就会变。.bundle文件则是 Addressables 或 AssetBundle 体系的产物每个 bundle 都对应一个哈希值Unity 加载时会通过哈希判断是否要重新下载。微信小游戏适配层也就是官方的weapp-adapter和 Unity 微信小游戏适配方案会把 Unity 的资源加载请求转成小游戏文件系统读写。这里有个很隐蔽的问题Unity 的 WebGL 加载器本身带缓存逻辑默认会把下载下来的资源直接存进文件系统文件名就是原始的.bundle名。如果我们在 CDN 上更新了同一个 bundle 文件的内容但路径不变Unity 侧加载器可能根本不会重新请求而是直接读取本地旧文件这就造成了“资源过期”。2. 缓存过期的本质版本号没有参与资源定位2.1 为什么迭代后用户看到的还是老资源一开始我排查的时候也走了弯路以为给 CDN 文件加一个?v1查询参数就能解决。实测发现微信小游戏环境里 Unity 的.bundle请求地址来自内部的 manifest 文件而这个 manifest 文件本身又是缓存在本地的。就算你在服务器上改了 bundle 的内容但 manifest 里记录的 hash 和 URL 没变Unity 就知道本地有这个 bundle压根不去请求。所以问题的根因不是“网络缓存”而是“Unity 的本地缓存记录优先”。换句话说微信小游戏的缓存过期方案核心不是清 CDN 缓存而是要让 Unity 侧感知到“本地文件已经和远程 manifest 不一致”。而 Unity Addressables 在 WebGL 上怎么判断 bundle 是否需要更新它会比对远程 Catalog 文件中的 hash 和本地已下载 bundle 的 hashhash 不一致才触发下载。这里的关键在于一个叫Catalog的 JSON 文件AddressablesContentCatalog.json它维护了所有 bundle 的依赖关系和 hash 值。所以版本迭代时只要Catalog更新了bundle 的 hash 就会变Unity 就应该能重新下载。但实际又冒出来一个问题Catalog文件本身也有缓存它也是通过本地文件系统读取的。如果Catalog一直没变或者变了的Catalog被旧版本覆盖整个更新链路就断掉了。2.2 微信小游戏运行时的目录结构与清理范围微信小游戏的本地文件系统是按用户维度存储的每个小游戏在设备上有一块独立空间通过wx.env.USER_DATA_PATH可以拿到。Unity 适配层默认会把资源放在这个目录下的某个子目录中。常见的目录结构大概是USER_DATA_PATH/UnityStorage或者适配层自定义目录里面存 wasm、framework、bundle 等。USER_DATA_PATH/__unity__开头的临时目录存启动时下载的资源。USER_DATA_PATH/files存持久化的文件。如果只是粗暴地整目录清理会有一个问题你告诉玩家更新然后一上来就把整个缓存目录删了玩家进入游戏后要重新下载所有远程资源流量消耗大体验差。所以合理的方案是“精确清理失效资源”而不是“全量清缓存”。要精确清理得从 Unity 加载器可访问的文件列表入手。在微信小游戏适配层中Unity 通过FS模块操作文件你可以临时在加载前后调用wx.getFileSystemManager().readdir看目录里有什么。但更稳妥的做法是我们自己在加版本控制时改造资源加载入口让 Unity 请求的资源 URL 带上版本信息。2.3 版本号应该放在哪里才能彻底解决过期网上很多方案是修改game.json里的资源版本字段或者在启动时强制wx.removeSavedFile。实测下来这些方法都治标不治本。真正管用的是实现“版本驱动的资源命名启动时校验”的组合策略资源文件命名带上内容哈希或版本号。比如icon_v2.png、level1_v3.bundle这样路径一变本地缓存自动失效。远程Catalog和 manifest 文件的内容更新后名称也变化强制 Unity 拉取新配置。启动时先从服务器拉一个version.json记录当前线上版本号和资源版本号和本地缓存的版本号比较不一致就清掉对应目录。这个思路看着简单实际落地时要注意很多细节。下面从我的实操流程讲起每一步都有具体代码和踩坑记录。3. 具体方案设计版本配置与启动校验3.1 第一步在 Unity C# 侧加上版本号管理Unity 侧需要先定义一套版本管理接口。我这边在 C# 里维护一个VersionConfig类包含appVersion和resVersion。appVersion对应小游戏迭代版本resVersion对应远程资源包的版本。每次发布版本时手动递增这两个字段同时写入一个静态常量供加载时用。public static class VersionConfig { // 手动维护每次发版递增大版本号 public const string AppVersion 1.3.0; public const string ResVersion 20240518_01; // 远程版本配置地址 public const string VersionConfigUrl https://your-cdn.com/game/config/version.json; // 本地缓存版本号键用于和远程比对 public const string LocalResVersionKey LOCAL_RES_VERSION_KEY; }实际发布流程中自动化的同学会写一个构建脚本在打包时把AppVersion和ResVersion生成到version.json里和游戏包一起发。我这里用常量是方便快速验证如果你们有 Jenkins 或 CI/CD 流程完全可以通过脚本改写这个文件避免手动修改漏掉。3.2 第二步服务器配置 version.jsonversion.json是缓存过期判断的唯一信源。我放的格式比较简单{ appVersion: 1.3.0, resVersion: 20240518_01, cdnBase: https://your-cdn.com/game/remote_v20240518_01, forceUpdate: false, updateTip: 发现新版本是否立即更新 }cdnBase这一项是重点。它不是固定的 CDN 根路径而是带着版本号的目录前缀。这样做的好处是只需把整个远程资源目录从remote_v20240518_01发成remote_v20240518_02所有 bundle 和 catalog 的请求地址都会发生变化Unity 适配层在本地找不到对应文件就会自动重新拉取。这比单纯改查询参数更可靠因为 Unity 的加载器常驻缓存不会因为 URL 加个参数就失效很多情况下缓存键是文件名而非完整 URL。但要注意改cdnBase会带来一个副作用旧资源不会主动删除会继续留在用户设备里日积月累可能撑爆小游戏缓存空间。所以我在后续方案里加了一层启动清理逻辑。3.3 第三步小游戏启动时读取远程配置流程是小游戏启动先拉取version.json再和本地缓存版本号比较。如果resVersion变了清理旧的远程资源缓存目录然后重新初始化 Unity。如果一样就沿用本地缓存直接启动减少加载时间。这里有个容易被忽略的点Unity 的 WebGL 加载器初始化时会自动尝试下载webgl.data.unityweb等基础文件如果我们在拉取version.json之前就让 Unity 启动可能就会出现 Unity 已经把旧资源写到本地了后面才得知版本过期导致清理时机太晚。所以需要在小游戏入口处先异步拉取版本配置再启动 Unity 主逻辑。如果用微信小游戏官方提供的 Unity 适配层通常入口是game.js里面会调用unityInstance的初始化。我们要在调用前插入版本检查代码。我这里用的是 JavaScript 写在小游戏入口逻辑里const fs wx.getFileSystemManager(); const LOCAL_VERSION_KEY LOCAL_RES_VERSION_KEY; const USER_DATA_PATH wx.env.USER_DATA_PATH; function getLocalResVersion() { try { const value wx.getStorageSync(LOCAL_VERSION_KEY); return value || ; } catch (e) { return ; } } function setLocalResVersion(version) { try { wx.setStorageSync(LOCAL_VERSION_KEY, version); } catch (e) { console.error(setLocalResVersion error, e); } } function cleanUnityRemoteCache() { try { const dirPath USER_DATA_PATH /UnityRemote; // 递归删除整个远程缓存目录 fs.rmdirSync(dirPath, true); } catch (e) { // 目录不存在时忽略 console.log(cleanUnityRemoteCache ignore:, e.errMsg); } } function checkVersionAndStart(callback) { wx.request({ url: https://your-cdn.com/game/config/version.json, timeout: 3000, success(res) { const remote res.data; const local getLocalResVersion(); if (remote.resVersion ! local) { cleanUnityRemoteCache(); setLocalResVersion(remote.resVersion); } callback(remote); }, fail(err) { // 网络请求失败时本地版本直接用, 避免卡启动 callback(null); } }); }这个小函数是整套方案的地基。网络失败时选择信任本地缓存是因为如果版本配置拉不到游戏连启动都做不到就太影响体验了最多是资源不更新但能先让玩家玩上等下一局或下一次启动再更新。不过如果你的游戏是强联网类型资源版本不一致会直接导致逻辑错乱那建议网络失败时弹窗重试不要进游戏。3.4 第四步把 CDN 基础路径注入到 Unity 加载配置version.json里的cdnBase怎么传给 Unity微信小游戏适配层在初始化 Unity 时有个loaderConfig或Module配置里面有companyName、productName、dataUrl、frameworkUrl、codeUrl等字段这些 URL 都是可以动态修改的。我这里的做法是拉取version.json成功后把cdnBase拼到资源路径上。具体的适配层差异比较大不同版本可能字段名不一样。我这里以我项目里用的中间层为例const remote ...; // version.json 返回的数据 const cdnBase remote.cdnBase; unityInstanceConfig { dataUrl: cdnBase /webgl.data.unityweb, frameworkUrl: cdnBase /webgl.wasm.framework.unityweb, codeUrl: cdnBase /webgl.wasm.code.unityweb, // 其他配置保持默认 };如果是老项目webgl.data.unityweb可能留在首包里那就不用动这个文件只需要处理远程 bundle 的地址。Addressables 在微信小游戏环境下的加载地址一般会经过一个自定义的LoadPath我们需要确保LoadPath的前缀从本地读取的cdnBase中拼接而来。这里有一个更优雅的方案构建时把cdnBase直接写进 Addressables 的 Profile 变量然后每次发版时只更新version.jsonunity 启动后先改cdnBase的全局变量再Addressables.ReinitializeAsync。我试过这个方案能跑通但版本间兼容性要求高如果旧代码里没有读取cdnBase的配置改了也白搭。所以我最后采用了更简单粗暴的“启动时修改适配层 URL 拼接 query 参数”的方式。3.5 第五步版本变化时清理旧缓存目录的时机清理目录不能乱来否则会出现一种情况玩家启动时正在加载资源你却把正在读的文件删了。所以清理动作要前置在 Unity 初始化前完成。同时为了避免清空整个目录导致首包资源也被删掉首包资源在小游戏包里不在USER_DATA_PATH所以一般不会误删清理目录要精准定位到远程资源目录。我使用的目录规则是USER_DATA_PATH/UnityRemote/{resVersion}每次resVersion更新后新版本使用新目录老版本的目录等待被清理。启动时检查当前本地存储的resVersion如果和远程不一致就遍历USER_DATA_PATH/UnityRemote下所有子目录把除新版本外的目录全部删除。这个延迟清理比一次性全删要好因为玩家第一次更新时如果网络不好新资源都没下载完旧资源全删了就会导致进游戏后疯狂下载。3.6 第六步微信小游戏分包和 FileSystemManager 的配合Unity 转微信小游戏通常会做首包拆分首包放启动场景和基础 DLL远程包放完整资源。微信小游戏的主包和分包有大小限制所以远程资源包怎么塞、怎么加载也是缓存过期方案的关键一环。我用的是“远程分包”和“unity WebGL remote”两种加载模型的结合。Unity 侧加载资源的请求最终会落到一个自定义协议处理上比如https://your-cdn.com/game/remote_v20240518_01/bundles/xxx.bundle。如果你没有对 URL 做版本前缀处理那么即使本地清了网络层也没问题但 Addressables 内部记录的资源路径还是旧路径会造成连环缓存。这个必须自己把LoadPath和cdnBase绑定清楚。另外微信小游戏的loadSubpackage和 Unity 的webgl远程资源加载是两套逻辑。如果你们有微信小游戏分包那更新时要留意分包更新靠的是微信后台配置的版本号不是你 CDN 上的版本号。我这边采用的做法是小游戏整体发新版时同时提升微信平台上的版本号确保微信主动替换小游戏包和分包而 Unity 远程资源包则靠version.json管理。4. 实操中遇到的高频问题和排查思路4.1 问题一改了配置玩家还是上报旧版本号我这里遇到最诡异的情况是version.json已经被更新了但线上玩家依然是旧版本号。查了半天发现version.json本身没有做缓存控制微信小游戏请求它时命中了 CDN 的缓存。虽然这个文件是用wx.request拉取的理论上微信不会强缓存但 CDN 节点上配置了Cache-Control: max-age86400导致玩家一天内拿到的都是旧文件。解决办法给version.json的请求加时间戳参数或者干脆设置 CDN 对该文件Cache-Control: no-cache。要特别注意小游戏wx.request的url如果加了时间戳version.json的缓存键会变化但 CDN 上还是会缓存原始 URL所以要防的是 CDN 层缓存不是微信层。最终我用的是const timestamp Date.now(); const versionUrl https://your-cdn.com/game/config/version.json?t timestamp;时间戳参数虽然丑但能确保每次启动都拿到最新配置。相比对Cache-Control的设置这个更简单且不会出现“同一个 CDN 节点有的更新有的没更新”的问题。4.2 问题二清理缓存后重新下载太慢玩家流失我刚开始做全量清理的时候清理完目录重新进游戏所有远程资源都要重新下载。因为资源都在 CDN速度取决于网络环境有些玩家在弱网下整整转圈几分钟自然就流失了。所以后来我把方案改成了“目录版本化 延迟清理”。具体做法是新版本下载时老版本目录先保留等新版本资源下载完成并成功进入游戏后再删掉老版本目录。这样做虽然设备空间占用会短暂多一份但能保证玩家在更新过程中不会面临“一无所有”的处境。等版本稳定后也可以在启动时统一清理所有非当前版本的目录只留最近一个旧版本兜底。另外还可以给下载过程加进度条。微信小游戏环境下 Unity 的加载进度可以从UnityLoader的onProgress拿这时配合下载百分比展示给玩家体验会好很多。我们线上版本必须有 loading 根因之一就是这里。4.3 问题三Unity 侧的 AssetBundle 缓存不完全受控如果你用的不是 Addressables而是原生AssetBundle.LoadFromFileAsync那在微信小游戏环境下的行为会有些不同。微信小游戏环境按AssetBundle路径读本地文件如果文件不存在会走 Unity 的 WebGL 预加载逻辑去请求。而如果你下载 bundle 后把文件放在了一个固定目录下次加载时会直接读旧文件资源包内引用发生变化时就可能出现“UI 贴图错乱但逻辑正常”。我的建议是AssetBundle 的资源文件名尽量用完整哈希值命名比如f4d0c2a1.bundle而不是ui_tex.bundle。这样内容一变文件名就变本地缓存自然失效。而vi版本号或resVersion变化时只需要把整个目录切到新的cdnBase即可。4.4 问题四小游戏平台强制缓存导致白屏有一种白屏现象是小游戏包内webgl.data.unityweb更新了但微信小游戏本地缓存的旧包没有换。这个问题的原因在微信小游戏平台本身开发者工具或线上版本如果包里的文件 hash 没变微信平台可能不会重新下载。最常见的是你只改了 CDN 上的远程资源却没有发布新版小游戏包于是玩家其实还是用旧包启动旧的game.js里记录的地址还是旧cdnBase。所以要想彻底解决远程资源版本变化必须和新版小游戏包绑定发布。我这边每次发版都会新建一个小游戏版本哪怕game.js里唯一的改动只是把cdnBase从常量改成读取version.json的动态配置也要让微信平台强制更新小游戏包这样才能让新版逻辑生效。4.5 常见问题速查表问题现象可能原因解决方案修改远程 bundle 后线上还是老资源Addressables catalog 缓存了旧的 hash远程cdnBase带上版本号强制改变资源路径version.json 拿到旧数据CDN 缓存请求 URL 后加时间戳并设置 no-cache清理缓存后重新下载太慢全量删除本地资源改为“版本目录延迟清理”策略保留一份旧版兜底白屏且无任何报错小游戏包内 js 和 wasm 还是旧版本发布新版小游戏包确保包内初始化逻辑更新进入关卡后出现旧材质旧 UIAssetBundle 同名但内容已变bundle 文件名用内容哈希避免同名覆盖弱网下启动卡 loading远程资源下载失败或超时添加重试机制超时后可选择先玩旧资源本地缓存目录越来越大未清理非当前版本目录启动时遍历版本目录删除过期目录5. 完整落地流程示例5.1 构建和发布的日常流程我自己的团队现在走的是这样一套流程Unity 侧构建 WebGL 产物远程资源包按resVersion目录上传到 CDN。提交微信小游戏包时game.js中内置读取version.json的逻辑cdnBase从远程配置读取。服务器发布version.json其中resVersion与当前 CDN 目录对应。玩家启动小游戏拉取version.json比对本地存储版本。不一致时清除非当前版本的缓存目录更新本地版本号启动 Unity。这套流程的好处是只要resVersion一变所有玩家都会自然切到新资源目录不会出现新旧资源混用。而且旧资源目录并不是立刻删除而是等新资源下载完成后在后台清除体验上安全很多。5.2 绕过缓存预加载的调试技巧开发调试时最烦的就是缓存干扰。我一般会在开发者工具里清除缓存后重新编译或者在game.js里临时加一个强制清理逻辑// 仅用于调试上线前必须删除 cleanUnityRemoteCache(); setLocalResVersion();这样每次启动都会强制走一次完整下载流程方便快速发现问题。但千万别把这个逻辑留在线上否则玩家每次启动都会重新下载所有资源。如果你用微信开发者工具还可以直接在“清除缓存”菜单中选择“清除全部缓存”包括文件缓存和授权数据。这个操作能模拟最干净的首次启动状态。5.3 关于代码层面如何和 Unity C# 交互这一步很多 Unity 开发者比较陌生。统一的做法是在 C# 侧暴露一个方法通过[DllImport(__Internal)]或Application.ExternalCall调用 JS 侧函数用来拿到当前的cdnBase或resVersion。我这边是在启动画面阶段调用 JS 获取版本信息然后传给Addressables初始化的配置[DllImport(__Internal)] private static extern string GetResVersion(); [DllImport(__Internal)] private static extern string GetCdnBase(); public IEnumerator Start() { // 等待 JS 层准备好 yield return new WaitForEndOfFrame(); string cdnBase GetCdnBase(); string resVersion GetResVersion(); // 用 cdnBase 拼接远程加载路径 Addressables.ClearAllCachedCatalogs(); // 初始化 Addressables 目录 var handle Addressables.LoadContentCatalogAsync(cdnBase /catalog.json, true); yield return handle; }注意微信小游戏环境的__Internal是适配层提供的一个桥接方式具体函数名要和 JS 侧保持一致。如果你们用的是 Tuanjie 或者第三方适配方案可能接口名会不同但核心逻辑一样Unity 启动时从 JS 层拿到当前资源版本和 CDN 地址。5.4 版本回退方案有时候新版本资源有严重问题需要回退到老版本。这时不要在 CDN 上直接覆盖老资源而是把version.json里的resVersion改回上一个值并把cdnBase指向上一个版本目录。这样玩家启动时发现本地版本和远程版本不一致会切到老目录。这里唯一要注意的是老版本目录千万不要在清缓存时被删掉了所以我在清理逻辑里会保留至少最近两个版本的目录防止回退时无资源可用。6. 延伸思考更强制的“强更”场景有些游戏是不能容忍资源版本分发的比如竞技类、强社交类新旧玩家如果不在同一个版本匹配和逻辑都会出问题。这时可以在version.json加一个forceUpdate字段如果为true游戏弹窗提示用户重启或下载最新版并且不允许进入游戏。在一些极端的版本不兼容情况下可以直接弹“版本已过期请退出重进”的弹窗。对于这种强更场景缓存清理要更激进必须确保玩家本地没有旧资源残留。我建议的做法是拉取到forceUpdate true时先清理所有远程资源目录。清理完成后调用game.restart()重新启动。如果game.restart()在某些平台上受限可以提示用户手动杀掉进程重进。不过这种策略不要常用因为会打断玩家体验。正常线上运营中资源增量更新、静默更新才是主流强制更新只在有严重 BUG 或底层协议不兼容时才用。7. 最后分享两点心得我在这套方案上踩的坑不少。第一点永远不要相信“上线后测一次没问题就等于没问题”缓存过期问题的复现条件依赖玩家设备状态、网络环境和上一个版本必须用老版本缓存覆盖到新版本的方式去测试。我自己的做法是保留一个旧版小游戏包安装包在开发者工具里先跑旧版等缓存落盘后再跑新版观察是否出现资源错乱。第二点version.json本身是整个方案的单一故障点。如果它挂了玩家进不了新版本。所以这个接口的稳定性、容灾、降级策略都要做好。我这边做了双 CDN 冗余version.json请求失败时本地版本照用并在游戏内静默重试一次不阻塞启动。这样即使配置中心挂了玩家还能继续玩本地旧资源不至于完全无法启动。缓存过期这个话题网上能找到的资料大多是理论性的真正落地时每个项目的产物结构、适配层版本、Unity 版本都有差异所以方案一定要自己验证。最核心的思路就一句话让资源版本号参与路径启动时强制刷新配置清理只清目录不是清全部。做到了这三点Unity 转微信小游戏的版本迭代就不会再被缓存坑得死去活来。