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)中等(需要下完整清单)
实现复杂度需要构建时计算简单
回滚支持不支持自动回滚支持降级到旧版本
适用场景标准商业项目强版本号语义的 SDK

2.3 版本校验的时机

2.3.1 启动时校验

启动时校验是 YooAsset 的默认行为。开发者无需手动调用,只要 PlayMode 设置为 HostPlayMode 并配置了 HostServerURL,YooAsset 就会在初始化时自动检查版本:

// 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.Show<UpdateHintDialog>();
        }
    }
}
2.3.3 手动校验

对于运营活动触发的强制更新(节假日活动、紧急 Bug 修复),可以通过运营后台下发"强制更新"指令:

public class ForcedUpdateChecker
{
    /// <summary>
    /// 在合适的时机(如打开商店、领取奖励)检查运营指令
    /// </summary>
    public async Task<bool> CheckForcedUpdate()
    {
        string url = $"{ServerConfig.ApiHost}/api/forced-update";
        var resp = await HttpGetAsync(url);
        if (resp.needForcedUpdate)
        {
            // 弹出强制更新界面
            UIManager.Show<ForcedUpdateDialog>(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.Show<ForceUpdateDialog>(config.updateUrl);
            return;
        }
        
        // 2. 初始化 YooAsset
        await YooAssets.InitializeAsync();
        
        // 3. 检查 YooAsset 资源版本
        var checkOp = YooAssets.RequestCheckVersionAsync();
        await checkOp.Task;
        
        if (checkOp.Result.IsNewVersion)
        {
            // 4. 提示玩家更新
            UIManager.Show<UpdateConfirmDialog>(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, Action<bool> 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 Task<string> 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 Task<bool> 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 Queue<float> _crashSamples = new Queue<float>();
    
    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 Task<RollbackResult> 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. 构建时就考虑灰度:把版本号设计为"主版本+灰度标签",如 1.0.5-rc.1
  2. 监控先行:灰度发布必须配套监控,没有监控的灰度是"裸奔"
  3. 回滚预案:每次发版前,运营、技术、QA 必须对齐回滚流程
  4. 多版本缓存:至少保留 3 个历史版本,平衡"磁盘占用"与"回滚能力"
  5. 强制更新慎用:频繁的强制更新会严重伤害玩家体验

下一篇差量更新与断点续传

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

淡海水

感谢支持 共同进步 好运++

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值