08-01-YooAsset热更新-Unity版本管理机制 热更新-版本管理机制篇章08-核心技术-热更新系统状态已完成阅读时间约 25 分钟一、引言1.1 本章在系列中的定位在 YooAsset 整个资源管理体系中版本管理是连接构建端与运行端的桥梁。如果把 YooAsset 想象成一套完整的物流系统那么版本管理就是快递单号系统构建端在打包时生成清单Manifest运行端在启动时校验单号只有单号对得上整个系统才能顺畅运转。对于一个商业级 Unity 项目来说版本管理是热更新Hot Update能否落地的第一道关卡。它看似简单背后却涉及本地清单的持久化、远端清单的获取、本地与远端清单的对比、灰度发布的分流、回滚时的版本切换等多个工程问题。1.2 本章要解决的核心问题YooAsset 的版本号体系如何设计数字版本与 Hash 版本如何选择游戏启动时YooAsset 如何判断是否需要更新强制更新与非强制更新在 YooAsset 中如何落地灰度发布如何在 YooAsset 上实现当新版本出现问题时如何快速回滚到旧版本1.3 阅读前置要求了解 YooAsset 的 Manifest 概念参考 02-原理篇-清单与寻址了解 YooAsset 的运行模式EditorPlayMode、OfflinePlayMode、HostPlayMode、WebPlayMode了解 YooAsset 的 WebServer 部署参考 06-源码深度-文件系统与下载模块二、版本校验2.1 版本校验的流程版本校验是 YooAsset 热更新的入口。当游戏在 HostPlayMode联机模式下启动时YooAsset 默认会执行以下流程[游戏启动] │ ▼ [读取本地缓存清单]若存在 │ ▼ [请求远端最新清单] ── HTTP GET PackageManifest_{Version}.hash / .bytes │ ▼ [对比本地与远端清单] │ ├── 一致 → 直接进入游戏无需下载 │ └── 不一致 → 触发下载流程 │ ▼ [下载差异资源] │ ▼ [更新本地清单] │ ▼ [进入游戏]YooAsset 中的版本校验主要发生在以下三个时机时机触发方式用途启动时自动触发检查是否需要更新运行时手动调用RequestCheckVersionAsync主动检查更新切场景业务层调用在场景切换间隙检查2.2 版本校验的实现2.2.1 Manifest 文件结构YooAsset 在构建时会在PackageManifest_{Version}.bytes中写入版本信息// 简化的 Manifest 头部实际为二进制 public class PackageManifest { public string PackageName; // 包名如 DefaultPackage public string PackageVersion; // 版本号如 1.0.5 public string PackageNote; // 备注可选 public bool IsIncludeMainAssetBundle; // 是否包含主 Bundle public string MainAssetBundleFileName; // 主 Bundle 名 public string[] AssetBundleList; // 所有 Bundle 文件名 public AssetInfo[] AssetList; // 所有资源条目 public BundleInfo[] BundleList; // 所有 Bundle 详情 public string Hash; // 整个 Manifest 的 Hash public string[] Dependencies; // 依赖的其他包 }2.2.2 Hash 对比YooAsset 在RequestCheckVersionAsync中执行的核心逻辑伪代码public class CheckVersionOperation : AsyncOperationBase { protected override void OnStart() { // 1. 下载远端清单索引仅一个小的 .hash 文件 var indexOp GetRemoteTextAsync(PackageManifest_{Version}.hash); // 2. 与本地记录的 Hash 对比 if (indexOp.Result _cachedHash) { // 远端无更新使用本地清单直接进入游戏 _status EOperationStatus.Succeed; } else { // 3. 远端有更新下载完整清单 var manifestOp GetRemoteTextAsync(PackageManifest_{Version}.bytes); // 4. 解析后保存到本地 SaveCacheFile(manifestOp.Result); } } }关键设计第一步只下载一个极小的.hash文件几十字节避免每次启动都拉取完整 Manifest只有 Hash 不一致时才下载完整的.bytes清单这种两步校验是大流量游戏的标准做法能极大降低 CDN 的 QPS 压力2.2.3 版本对比YooAsset 默认采用 Hash 对比方式但也支持数字版本号对比。开发者可以在IBuildPipeline中自定义版本号生成策略// 自定义版本号日期构建号GitHash public class CustomBuildPipeline : IBuildPipeline { public string GetPackageVersion() { string date DateTime.Now.ToString(yyyyMMdd); string build PlayerSettings.bundleVersion; string gitHash GetGitShortHash(); // 通过命令行获取 return ${date}.{build}.{gitHash}; } }两种版本对比方式的差异维度Hash 对比推荐数字版本对比精确度字节级一致零误差字符串级可能漏判服务器压力极低只下 .hash中等需要下完整清单实现复杂度需要构建时计算简单回滚支持不支持自动回滚支持降级到旧版本适用场景标准商业项目强版本号语义的 SDK2.3 版本校验的时机2.3.1 启动时校验启动时校验是 YooAsset 的默认行为。开发者无需手动调用只要PlayMode设置为HostPlayMode并配置了HostServerURLYooAsset 就会在初始化时自动检查版本// YooAssetRuntimeSetting public class YooAssetSetting : ScriptableObject { public EPlayMode PlayMode; // HostPlayMode public string HostServerURL; // https://cdn.example.com/v1/ public string FallbackHostServerURL; // 备用地址 public bool AutoCheckVersion; // 是否自动检查版本 }2.3.2 运行时校验对于需要长时间挂机的游戏例如放置类、SLG玩家可能几天都不重启游戏。此时应在合适的时机主动调用版本检查public class GameEntry : MonoBehaviour { /// summary /// 在切场景时检查更新避免影响玩家体验 /// /summary public async void CheckUpdateOnSceneChanged() { var op YooAssets.RequestCheckVersionAsync(30); // 30秒超时 await op.Task; if (op.Status EOperationStatus.Succeed op.Result.IsNewVersion) { // 提示玩家有更新 UIManager.ShowUpdateHintDialog(); } } }2.3.3 手动校验对于运营活动触发的强制更新节假日活动、紧急 Bug 修复可以通过运营后台下发强制更新指令public class ForcedUpdateChecker { /// summary /// 在合适的时机如打开商店、领取奖励检查运营指令 /// /summary public async Taskbool CheckForcedUpdate() { string url ${ServerConfig.ApiHost}/api/forced-update; var resp await HttpGetAsync(url); if (resp.needForcedUpdate) { // 弹出强制更新界面 UIManager.ShowForcedUpdateDialog(resp.updateUrl); return true; } return false; } }三、强制/非强制更新3.1 强制更新强制更新是指版本不一致时阻止游戏继续运行要求玩家必须更新才能进入。强制更新通常用于关键 Bug 修复例如支付、登录模块的严重问题安全更新例如反作弊模块更新协议变更例如登录协议或支付协议变更法律合规例如 GDPR、未成年人保护相关更新强制更新的实现需要将是否强制更新的判断从 YooAsset 内部剥离出来交给运营后台控制[游戏启动] │ ▼ [读取 YooAsset 本地清单] ──→ 清单存在 │ │ │ ├── 不存在 → 首次启动进入下载流程 │ ▼ [请求运营后台的版本配置] ──→ HTTP GET /api/game-config │ ▼ [运营后台判断] │ ├── 强制更新 → 弹出强制更新界面玩家必须更新 │ └── 非强制更新 → 进入 YooAsset 下载流程 │ ▼ [下载完成后弹出可选更新提示]实现代码示例public class UpdateGate : MonoBehaviour { async void Start() { // 1. 查询运营后台的版本策略 var config await GameApi.GetVersionConfig(); if (config.mustUpdate) { // 强制更新阻止游戏继续 UIManager.ShowForceUpdateDialog(config.updateUrl); return; } // 2. 初始化 YooAsset await YooAssets.InitializeAsync(); // 3. 检查 YooAsset 资源版本 var checkOp YooAssets.RequestCheckVersionAsync(); await checkOp.Task; if (checkOp.Result.IsNewVersion) { // 4. 提示玩家更新 UIManager.ShowUpdateConfirmDialog(async (accepted) { if (accepted) { await DownloadAndUpdate(); } else { // 玩家选择跳过但功能可能受限 EnterGame(); } }); } else { // 无需更新 EnterGame(); } } }3.2 非强制更新非强制更新是指版本不一致时提示玩家更新但玩家可以选择跳过。非强制更新适用于新玩法上线例如新角色、新地图新活动开启例如节日活动、签到活动小优化例如美术资源替换、UI 调整非强制更新需要在 UI 上做出明确的立即更新和稍后再说两个选项public class UpdateConfirmDialog : MonoBehaviour { public TMP_Text sizeText; public Button updateButton; public Button skipButton; private long _downloadSize; public void Setup(long size, Actionbool callback) { _downloadSize size; sizeText.text $需要下载 {size / 1024 / 1024} MB 新资源; updateButton.onClick.AddListener(() { callback(true); Close(); }); skipButton.onClick.AddListener(() { callback(false); Close(); }); } }3.3 更新策略的配置3.3.1 策略选择策略适用场景用户体验运营风险强制更新关键 Bug、安全问题较差低强提示更新重要玩法、核心资源中等中弱提示更新新活动、新角色较好中静默更新美术资源、UI 调整好高不更新1% 不到的边缘资源最好最高3.3.2 更新界面定制YooAsset 本身不提供 UI开发者需要自行实现更新界面。推荐使用分层 UI 设计更新界面层次 ├── 强制更新遮罩不可关闭 │ ├── Logo │ ├── 当前版本号 │ ├── 进度条 │ └── 重启按钮更新完成后 │ ├── 强提示弹窗可关闭 │ ├── 标题 │ ├── 更新内容 │ ├── 大小 │ ├── 立即更新按钮 │ └── 稍后再说按钮 │ └── 静默提示条可关闭 └── 有新内容可下载3.3.3 更新进度的显示YooAsset 的下载进度通过DownloadUpdateOperation暴露var op YooAssets.DownloadUpdateAsync( downloadingMaxNum: 10, // 最大并发下载数 failedTryAgain: 3, // 失败重试次数 timeout: 60 // 单文件超时秒 ); while (!op.IsDone) { // 显示进度 progressBar.value op.Progress; progressText.text ${op.CurrentDownloadBytes / 1024 / 1024}MB / {op.TotalDownloadBytes / 1024 / 1024}MB; speedText.text ${op.CurrentSpeed / 1024:F1} KB/s; await Task.Yield(); }四、灰度发布4.1 灰度发布的概念灰度发布Gray Release是指先向小部分用户发布新版本观察无问题后再扩大发布范围的发布方式。其核心思想是用最小成本验证新版本。YooAsset 本身不直接实现灰度发布但提供了完整的扩展点。灰度发布通常在 CDN 层面、运营后台层面、客户端层面三个位置实现层次实现方式优缺点CDN 层面DNS 解析、CDN 边缘节点分流精准但需要 CDN 厂商支持运营后台层面按用户 ID 决定返回哪个版本清单灵活可精细化控制客户端层面客户端根据自身 ID 选择请求哪个版本简单但流量浪费4.2 灰度发布的实现4.2.1 运营后台的灰度配置最常见的灰度实现是运营后台返回不同的清单 URL// 运营后台的灰度配置示意 { grayEnabled: true, grayPercent: 10, // 灰度 10% 用户 grayUserList: [], // 白名单用户 grayVersion: 1.0.6, // 灰度版本 stableVersion: 1.0.5 // 稳定版本 }public class GrayReleaseManager { public async Taskstring GetManifestUrlAsync(string packageName, string userId) { var config await GameApi.GetReleaseConfig(); if (!config.grayEnabled) return ${config.cdnHost}/{packageName}/{config.stableVersion}/; // 检查白名单 if (config.grayUserList.Contains(userId)) return ${config.cdnHost}/{packageName}/{config.grayVersion}/; // 按 Hash 散列 int hash Math.Abs(userId.GetHashCode()) % 100; if (hash config.grayPercent) return ${config.cdnHost}/{packageName}/{config.grayVersion}/; return ${config.cdnHost}/{packageName}/{config.stableVersion}/; } }4.2.2 灰度的比例控制灰度比例的提升需要循序渐进5% → 20% → 50% → 100% 每阶段持续24-48 小时 监控指标崩溃率、ANR 率、卡顿率、关键路径成功率4.2.3 灰度的监控灰度期间需要重点监控以下指标指标阈值异常处理崩溃率上升超过 0.1%立即停止灰度启动失败率上升超过 0.5%立即停止灰度资源加载失败率上升超过 0.1%检查 CDN 与清单网络超时率上升超过 1%检查 CDN 节点玩家投诉量上升超过 10%评估是否回滚4.3 灰度发布的策略4.3.1 按比例灰度按用户比例灰度是最简单的方式缺点是同服玩家可能版本不同对联机游戏不友好。4.3.2 按设备灰度按设备类型高端机、中端机、低端机灰度public string GetGrayVersion() { int systemMemory SystemInfo.systemMemorySize; if (systemMemory 6144) // 6GB 高端机 return 1.0.6-high; else if (systemMemory 3072) // 3-6GB 中端机 return 1.0.6-mid; else // 3GB 以下低端机 return 1.0.5; // 不参与灰度 }4.3.3 按地区灰度按地区灰度可以避免全国性故障特别适合新内容需要运营配合的场景public string GetGrayVersion(string region) { // 新玩法先在广东、上海试点 var pilotRegions new[] { guangdong, shanghai, beijing }; if (pilotRegions.Contains(region)) return 1.0.6; else return 1.0.5; }五、回滚5.1 回滚的概念回滚Rollback是指将版本回退到之前的稳定版本。回滚与灰度是一体两面灰度是新版本的小流量验证回滚是验证失败后的紧急恢复。YooAsset 的回滚机制基于多版本缓存本地缓存中保留最近的 N 个版本清单必要时切换到旧版本清单。5.2 回滚的实现5.2.1 多版本共存YooAsset 的本地缓存目录设计{yooasset_cache_root}/ ├── DefaultPackage/ │ ├── PackageManifest_1.0.3.bytes │ ├── PackageManifest_1.0.3.bytes.meta │ ├── PackageManifest_1.0.4.bytes │ ├── PackageManifest_1.0.4.bytes.meta │ ├── PackageManifest_1.0.5.bytes │ ├── PackageManifest_1.0.5.bytes.meta │ └── ... ├── bundle_v1.0.3/ │ ├── bundle_01 │ ├── bundle_02 │ └── ... ├── bundle_v1.0.4/ └── bundle_v1.0.5/为什么需要多版本共存如果新版本1.0.6出现问题运营可以下发指令让客户端切换到1.0.5玩家本地已有1.0.5的资源无需重新下载这就是秒级回滚5.2.2 版本切换YooAsset 的版本切换通过修改激活清单实现public class VersionRollback { /// summary /// 切换到指定版本 /// /summary public async Taskbool RollbackToVersion(string targetVersion) { // 1. 检查本地是否存在该版本 var manifestPath $PackageManifest_{targetVersion}.bytes; if (!File.Exists(manifestPath)) { Debug.LogError($本地无版本 {targetVersion} 的清单); return false; } // 2. 强制切换清单 YooAssets.ForceChangeVersion(targetVersion); // 3. 重新初始化资源系统 await YooAssets.InitializeAsync(); return true; } }5.2.3 回滚验证回滚后需要进行一系列验证验证项验证方式失败处理启动验证重启游戏进入登录界面切回上一版本资源加载加载核心场景、UI、角色切回上一版本网络请求走完整网络流程切回上一版本数据兼容检查玩家存档是否兼容提供回档或转档工具5.3 回滚的策略5.3.1 自动回滚通过监控异常率自动触发回滚public class AutoRollbackMonitor { private float _sampleWindow 300; // 5 分钟采样窗口 private float _crashThreshold 0.005f; // 崩溃率阈值 0.5% private Queuefloat _crashSamples new Queuefloat(); public void RecordCrash(bool crashed) { _crashSamples.Enqueue(crashed ? 1f : 0f); while (_crashSamples.Count 100) _crashSamples.Dequeue(); if (_crashSamples.Count 100) { float crashRate _crashSamples.Average(); if (crashRate _crashThreshold) { // 触发自动回滚 RollbackManager.TriggerRollback(1.0.5, auto: high crash rate); } } } }5.3.2 手动回滚运营人员通过后台工具触发public class ManualRollback { /// summary /// 运营后台调用紧急回滚到上一版本 /// /summary public async TaskRollbackResult Rollback(string targetVersion, string reason) { // 1. 记录回滚原因用于事后分析 await GameApi.LogRollback(targetVersion, reason); // 2. 全服广播 await GameApi.BroadcastRollbackNotice(targetVersion); // 3. 强制玩家下次启动时回滚 PlayerPrefs.SetString(ForcedRollbackVersion, targetVersion); return RollbackResult.Success; } }5.3.3 渐进式回滚对于需要撤回到旧版本但保留新版本部分特性的场景public class GradualRollback { /// summary /// 渐进式回滚先回滚核心资源再回滚次要资源 /// /summary public async Task GradualRollbackAsync() { // 第 1 步回滚战斗相关资源 var battleRes new[] { battle_bundles, skill_bundles }; await RollbackBundles(battleRes, 1.0.5); // 第 2 步观察 24 小时 await Task.Delay(TimeSpan.FromHours(24)); // 第 3 步回滚 UI 相关资源 var uiRes new[] { ui_bundles, lobby_bundles }; await RollbackBundles(uiRes, 1.0.5); } }六、总结6.1 本章要点回顾版本校验YooAsset 默认采用 Hash 对比启动时自动检查启动性能损耗极低强制/非强制更新强制更新用于关键 Bug、非强制更新用于新内容UI 需分层设计灰度发布推荐在运营后台实现按用户 ID 散列分流可按设备/地区细分回滚基于多版本缓存实现秒级回滚可结合自动监控实现智能回滚6.2 与前后章节的关联前章08-00 / 07 章主要讨论构建与打包本章是其下游构建出来的清单如何被客户端消费后章08-02 将深入差量更新与断点续传解决清单不一致时如何高效下载6.3 实践建议构建时就考虑灰度把版本号设计为主版本灰度标签如1.0.5-rc.1监控先行灰度发布必须配套监控没有监控的灰度是裸奔回滚预案每次发版前运营、技术、QA 必须对齐回滚流程多版本缓存至少保留 3 个历史版本平衡磁盘占用与回滚能力强制更新慎用频繁的强制更新会严重伤害玩家体验下一篇差量更新与断点续传

本月热点