打包与发布
通过前几篇文章的讲解,我们已经完成了 HybridCLR 热更新框架的搭建、资源加载系统的完善以及业务逻辑的编写。本文将详细介绍如何将项目打包发布到 iOS 和 Android 平台,以及如何管理与发布热更新资源。学习完本文后,你将掌握 HybridCLR 项目的完整发布流程。
一、iOS 打包
iOS 平台的打包是 HybridCLR 项目中最具挑战性的环节。由于苹果对代码执行的严格限制,我们需要特别注意 AOT 编译与热更新代码的协调工作。
1.1 Xcode 项目配置
在 Unity 中构建 iOS 工程后,会生成一个 Xcode 项目。我们需要在构建前后通过脚本自动化配置 Xcode 项目,以确保 HybridCLR 正常运行。Unity 提供了 UnityEditor.iOS.Xcode 命名空间下的 API,可以直接操作 PBX 项目文件和 Plist 文件,这使得我们能够以代码方式管理构建配置,避免每次手动修改的繁琐操作。
PBXProject API 核心用法
在使用 Xcode API 时,最重要的类是 PBXProject,它代表了整个 Xcode 项目文件。通过它可以获取 target 的 GUID、修改 Build Properties、添加框架依赖和资源配置文件。常见的用法包括:
// 获取主 target 和测试 target 的 GUID
string mainTarget = pbxProject.GetUnityMainTargetGuid();
string unityFrameworkTarget = pbxProject.GetUnityFrameworkTargetGuid();
// 添加系统框架依赖
pbxProject.AddFrameworkToProject(mainTarget, "StoreKit.framework", false);
pbxProject.AddFrameworkToProject(mainTarget, "AdSupport.framework", false);
// 添加编译标志
pbxProject.AddBuildProperty(mainTarget, "GCC_PREPROCESSOR_DEFINITIONS", "HYBRIDCLR_ENABLED");
// 设置开发团队(自动签名时需要)
pbxProject.SetBuildProperty(mainTarget, "DEVELOPMENT_TEAM", "YOUR_TEAM_ID");
代码签名配置
iOS 打包的另一个关键环节是代码签名。如果使用自动化构建(如 Jenkins 或 GitHub Actions),需要配置导出选项。我们可以通过构建后的脚本生成 ExportOptions.plist 文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
注意 uploadBitcode 必须设置为 false,否则苹果服务器在重新编译 Bitcode 时会因为 HybridCLR 的解释器代码而失败。
BuildHandler 脚本的编写
在 Unity 编辑器下,我们可以通过 IPreprocessBuildWithReport 和 IPostprocessBuildWithReport 接口来自定义构建前后的处理逻辑。对于 iOS 平台,核心工作包括:启用 bitcode、设置 Linker Flags、配置 Info.plist 权限等。
以下是一个完整的 iOS BuildHandler 示例:
using System.IO;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.iOS.Xcode;
using UnityEngine;
public class iOSBuildHandler : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
public int callbackOrder => 0;
public void OnPreprocessBuildWithReport(BuildReport report)
{
if (report.summary.platform != BuildTarget.iOS)
return;
Debug.Log($"[iOSBuildHandler] 预构建开始,目标路径: {report.summary.outputPath}");
// 确保 HybridCLR 热更新 dll 已生成
string hybridCLRSettingsPath = "Assets/HybridCLR/HybridCLRSettings.asset";
if (!File.Exists(hybridCLRSettingsPath))
{
Debug.LogError("[iOSBuildHandler] 未找到 HybridCLR 配置文件,请先通过 HybridCLR/Generate All 生成热更新 dll");
}
}
public void OnPostprocessBuildWithReport(BuildReport report)
{
if (report.summary.platform != BuildTarget.iOS)
return;
string xcodeProjectPath = report.summary.outputPath;
Debug.Log($"[iOSBuildHandler] 后处理开始,Xcode 项目路径: {xcodeProjectPath}");
string pbxProjectPath = PBXProject.GetPBXProjectPath(xcodeProjectPath);
var pbxProject = new PBXProject();
pbxProject.ReadFromFile(pbxProjectPath);
string targetGuid = pbxProject.GetUnityMainTargetGuid();
// 禁用 Bitcode
pbxProject.SetBuildProperty(targetGuid, "ENABLE_BITCODE", "NO");
// 设置 Linker Flags,确保 aot dll 可以正确链接
pbxProject.AddBuildProperty(targetGuid, "OTHER_LINKER_FLAGS", "-Wl,-all_load");
// 设置 iOS 最低版本
pbxProject.SetBuildProperty(targetGuid, "IPHONEOS_DEPLOYMENT_TARGET", "12.0");
pbxProject.WriteToFile(pbxProjectPath);
// 处理 Info.plist
string plistPath = Path.Combine(xcodeProjectPath, "Info.plist");
var plist = new PlistDocument();
plist.ReadFromFile(plistPath);
plist.root.SetString("NSPhotoLibraryUsageDescription", "用于保存游戏截图");
plist.root.SetString("NSMicrophoneUsageDescription", "用于语音聊天功能");
plist.WriteToFile(plistPath);
Debug.Log("[iOSBuildHandler] Xcode 项目配置完成");
}
}
该脚本通过 Unity 的 Xcode API(UnityEditor.iOS.Xcode 命名空间)直接操作 .pbxproj 文件,无需手动在 Xcode 中修改配置,实现了构建流程的自动化。
1.2 HybridCLR 的 iOS 适配
iOS 平台对动态代码执行有严格的限制,HybridCLR 在 iOS 上通过以下机制实现热更新:
AOT 泛型处理
iOS 的 AOT 编译机制要求我们在构建前必须处理所有可能用到的泛型实例化。HybridCLR 提供了 HybridCLR.Editor.AOT.GenericReferenceGenerator,可以为 AOT 程序集补充泛型实例化信息。在第 42 篇(AOT 泛型原理)中我们详细讨论过这个问题,此处不再赘述。
Strip Engine Code 设置
在 iOS 构建时,Unity 的"Strip Engine Code"选项会裁剪未使用的引擎代码以减小包体。但 HybridCLR 热更新程序集中可能引用了这些被裁剪的代码,导致运行时崩溃。解决方案是在 link.xml 中保留这些引用:
<linker>
<assembly fullname="mscorlib" preserve="all" />
<assembly fullname="System" preserve="all" />
<assembly fullname="System.Core" preserve="all" />
<assembly fullname="UnityEngine">
<type fullname="UnityEngine.UI.*" preserve="all" />
<type fullname="UnityEngine.EventSystems.*" preserve="all" />
</assembly>
<assembly fullname="UnityEngine.UI" preserve="all" />
</linker>
Bitcode 注意事项
虽然我们在 BuildHandler 中禁用了 Bitcode,但需要了解其含义。Bitcode 是苹果的中间代码格式,App Store 连接后会将其重新编译为最终的可执行文件。HybridCLR 的 AOT + Interpreter 混合运行模式与 Bitcode 存在兼容性问题,因此必须禁用 Bitcode。如果不上架 App Store(如企业分发),禁用 Bitcode 没有任何影响。
1.3 常见打包问题
__Internal 链接错误
在 iOS 构建时,如果遇到如下错误:
Undefined symbol: __Internal
通常是因为 Native Plugins 引用了 __Internal 但未正确链接。解决方法是在 Xcode 的 Other Linker Flags 中添加 -ldl 和 -all_load。
AOT 缺少元数据
运行时出现 ExecutionEngineException: Attempting to call method 'xxx' for which no AOT code was generated.
这是因为热更新代码中调用了未被 AOT 编译的泛型方法。解决方案:
- 在
HybridCLR/Generate/AOTGenericReference中补充泛型实例化 - 或在
Assets/link.xml中保留对应的程序集
Il2Cpp 代码剥离问题
iOS 使用 IL2CPP 后端,代码剥离(Managed Stripping)级别建议设置为 Medium 或 Low,过高的剥离级别(如 High)可能导致热更新代码中引用的类型被错误地裁剪。
二、Android 打包
相比 iOS,Android 平台的打包相对友好,但仍有诸多细节需要关注。
2.1 Gradle 配置
Unity 2019.4 及以上版本使用 Gradle 作为 Android 构建系统。我们需要正确配置 gradle.properties、build.gradle 以及 AndroidManifest.xml。Unity 在导出 Gradle 项目时提供了自定义模板机制,允许我们通过 Assets/Plugins/Android/ 目录下的模板文件来控制构建输出。
签名配置与多渠道打包
在实际项目中,我们通常需要针对不同渠道(如华为、小米、OPPO、应用宝等)生成不同的包。Unity 支持通过 Android Keystore 管理器配置签名,但更灵活的做法是在 Gradle 层配置签名信息:
android {
signingConfigs {
release {
storeFile file("release.keystore")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
debug {
// 使用默认的 debug 签名
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
// 多渠道配置(使用 productFlavors)
flavorDimensions "channel"
productFlavors {
official {
dimension "channel"
applicationId "com.yourgame.official"
versionNameSuffix "-official"
}
huawei {
dimension "channel"
applicationId "com.yourgame.huawei"
versionNameSuffix "-huawei"
}
xiaomi {
dimension "channel"
applicationId "com.yourgame.xiaomi"
versionNameSuffix "-xiaomi"
}
}
}
多渠道打包完成后,每个渠道包的 HybridCLR 热更新逻辑保持一致,区别仅在于渠道标识(可以通过 Application.identifier 区分)和 SDK 接入差异。渠道 SDK 的接入通常会引入独立的第三方库,这些库在主工程中以 AOT 程序集形式存在,热更新代码中引用它们时需要注意前面章节讨论过的补充元数据问题。建议将渠道 SDK 的调用接口封装在桥接层中,热更新代码通过桥接接口访问渠道功能,这样即使渠道 SDK 升级也无需修改热更新代码。
Gradle 构建配置示例
以下为 Unity 导出 Gradle 项目后,建议的自定义 mainTemplate.gradle 配置:
apply plugin: 'com.android.application'
android {
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.yourcompany.game"
minSdkVersion 21
targetSdkVersion 33
versionCode 1
versionName "1.0.0"
ndk {
// HybridCLR 要求同时保留 armeabi-v7a 和 arm64-v8a
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
}
}
// 开启 Java 8 支持
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// AAB 构建配置
bundle {
language {
enableSplit = false
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
2.2 HybridCLR 的 Android 适配
Android 平台同样使用 IL2CPP 后端,适配要点与 iOS 类似,但有几点不同。最大的差异在于 Android 生态的碎片化问题——不同厂商、不同系统版本的设备对 IL2CPP 运行时行为存在细微差异,这就要求我们在适配时更加关注兼容性测试的覆盖面。以下是需要重点关注的配置项。
IL2CPP 设置
在 Unity 的 Player Settings 中,Android 平台的 IL2CPP 相关设置如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Scripting Backend | IL2CPP | HybridCLR 必须使用 IL2CPP |
| Managed Stripping Level | Low 或 Medium | High 可能导致热更新代码引用被裁剪 |
| Target Architecture | ARM64 | 建议 ARM64 + ARMv7 双架构 |
| Minimum API Level | API Level 21+ | 覆盖 95% 以上设备 |
AAB 与 APK 构建的选择
| 构建格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| APK | 可直接安装测试 | 包体较大 | 内部测试、开发调试 |
| AAB (Android App Bundle) | 按需分发,包体更小 | 需上传 Google Play | 正式上线 Google Play |
如果使用 AAB 格式,需要注意 HybridCLR 的 AOT 元数据会被打包进基础模块(base module),热更新 dll 则通过 AssetBundle 动态下载,这与第 43 篇(资源加载系统)中实现的远程下载流程一致。在 AAB 构建时,Unity 会根据应用商店的分发策略自动拆分 ABIs、纹理格式和语言资源,这意味着部分玩家下载的安装包可能只包含特定架构的代码。对于 HybridCLR 来说,必须确保所有 ABI 架构的 IL2CPP 原生代码都被包含在基础模块中,否则可能出现"设备支持但运行闪退"的问题。
ProGuard 混淆规则
如果开启代码混淆,需要在 proguard-rules.pro 中添加 HybridCLR 相关的保留规则:
-keep class HybridCLR.** { *; }
-keep class UnityEngine.** { *; }
-keepattributes *Annotation*
-keep class com.unity3d.player.** { *; }
-keep class * extends com.unity3d.player.UnityPlayerActivity
2.3 常见打包问题
Gradle 构建失败
错误信息类似 Could not find com.android.tools.build:gradle:7.0.0,通常是因为 Gradle 版本与 Android SDK 版本不匹配。检查以下设置:
- 确保安装了对应版本的 Android SDK Build-Tools
- 在
Preferences/External Tools/Android中设置正确的 SDK 路径 - 尝试切换 Gradle 版本(在
Assets/Plugins/Android/mainTemplate.gradle中指定)
libil2cpp.so 找不到符号
构建成功后运行时闪退,日志显示 java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol。这通常是因为 Native Plugin 的 .so 文件未正确包含进 APK/AAB。检查:
.so文件是否放在Assets/Plugins/Android/libs/下- ABI 目录是否正确(
armeabi-v7a/arm64-v8a)
资源加载路径问题
Android 平台对文件路径大小写敏感。确保热更新资源的下载路径与加载路径完全一致。我们在第 44 篇(资源热更新系统)中实现的地址映射表已经处理了此问题,但需要确认构建时 AssetBundle 的名称未被意外修改。
三、热更新资源的发布
应用包体发布到应用商店后,业务逻辑和资源的更新就完全交由 HybridCLR 的热更新系统来管理了。
3.1 CDN 配置
热更新资源需要部署到 CDN(内容分发网络)上,以确保全球玩家都能以较低的延迟下载更新。国内常用的 CDN 服务商包括阿里云 CDN、腾讯云 CDN、七牛云等;海外市场则推荐使用 AWS CloudFront、Cloudflare 或阿里云海外节点。选择 CDN 时需要考虑资源覆盖区域、计费方式和 HTTPS 支持等因素。
HTTPS 与资源完整性校验
热更新资源在传输过程中必须使用 HTTPS 协议,防止中间人攻击导致用户下载到被篡改的代码或资源。同时,客户端在下载完成后需要对文件进行完整性校验,确保文件未被损坏或篡改:
public static bool VerifyFileIntegrity(string filePath, string expectedMd5)
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
using (var stream = File.OpenRead(filePath))
{
byte[] hash = md5.ComputeHash(stream);
StringBuilder sb = new StringBuilder();
foreach (byte b in hash)
sb.Append(b.ToString("x2"));
string fileMd5 = sb.ToString();
return fileMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase);
}
}
}
完整性校验的 MD5 值可以在 version.txt 中为每个资源文件单独声明,也可以作为 AssetBundle 清单文件的一部分在构建时生成。推荐的做法是在构建 AssetBundle 时一并生成资源清单文件,其中记录每个文件的 MD5 值和大小,上传到 CDN 后由客户端在下载时进行校验。
CDN 目录结构设计
合理的 CDN 目录结构应当支持多版本、多平台、多渠道的并行发布:
https://cdn.yourgame.com/
├── hotupdate/
│ ├── version.txt # 版本索引文件
│ ├── v1.0.0/
│ │ ├── Windows/
│ │ ├── Android/
│ │ └── iOS/
│ ├── v1.1.0/
│ │ ├── Windows/
│ │ ├── Android/
│ │ └── iOS/
│ └── v1.2.0/
│ ├── Windows/
│ ├── Android/
│ └── iOS/
└── assets/
└── ...
每个版本目录下存放对应平台的 AssetBundle 包和 HybridCLR 热更新 dll(加密后)。version.txt 文件的内容格式如下:
{
"latestVersion": "1.2.0",
"versions": {
"1.0.0": {
"url": "https://cdn.yourgame.com/hotupdate/v1.0.0/",
"mandatory": false,
"releaseDate": "2025-06-01",
"description": "初始版本"
},
"1.1.0": {
"url": "https://cdn.yourgame.com/hotupdate/v1.1.0/",
"mandatory": true,
"releaseDate": "2025-06-15",
"description": "修复战斗系统崩溃问题"
},
"1.2.0": {
"url": "https://cdn.yourgame.com/hotupdate/v1.2.0/",
"mandatory": false,
"releaseDate": "2025-07-01",
"description": "新增端午节活动"
}
}
}
CDN 缓存策略
CDN 节点默认会对静态资源进行缓存,这可能导致玩家下载到旧版本资源。推荐的缓存策略:
- 热更新 dll 文件:设置
Cache-Control: no-cache或不缓存 - AssetBundle 文件:短缓存期(如 10 分钟)配合 Hash 文件名
- version.txt:不缓存,每次检查更新时直接回源
CDN 预热也是一个容易被忽视的环节。新版本发布前,建议手动或通过 API 触发 CDN 预热,将热度较高的资源预先部署到各边缘节点,避免上线初期大量用户同时回源导致源站压力过大。预热完成后,还需要通过各地监测节点验证资源可访问性,确保覆盖所有目标区域后再开放客户端更新入口。
3.2 版本管理
版本管理是热更新体系中最容易被忽视却最关键的环节。
版本号规范
推荐使用三段式版本号:主版本.次版本.修订号
| 版本级别 | 变更范围 | 是否强制更新 | 示例 |
|---|---|---|---|
| 主版本 | 重大功能重构或架构变更 | 是 | 2.0.0 |
| 次版本 | 新增功能或玩法 | 否 | 1.3.0 |
| 修订号 | Bug 修复或资源替换 | 否(可标记强制) | 1.2.1 |
版本号的管理应与 Unity 的 PlayerSettings.bundleVersion 和 Application.version 关联,保证客户端版本与服务端版本一致。
版本比较逻辑
客户端启动时,会从 CDN 拉取 version.txt,与本地的 PlayerPrefs 中记录的上次版本号进行比较。比较逻辑如下:
public static class VersionManager
{
public static bool ShouldUpdate(string localVersion, string remoteVersion)
{
Version local = new Version(localVersion);
Version remote = new Version(remoteVersion);
// 远程版本更高时才需要更新
return remote > local;
}
public static bool IsMandatoryUpdate(VersionConfig config, string localVersion)
{
// 遍历从本地版本到最新版本之间的所有版本
// 如果任何一个版本标记为强制更新,则本次更新为强制
foreach (var kv in config.versions)
{
if (kv.Value.mandatory &&
IsVersionInRange(localVersion, kv.Key, config.latestVersion))
{
return true;
}
}
return false;
}
private static bool IsVersionInRange(string local, string target, string latest)
{
Version vLocal = new Version(local);
Version vTarget = new Version(target);
Version vLatest = new Version(latest);
return vLocal < vTarget && vTarget <= vLatest;
}
}
3.3 灰度发布
灰度发布(Canary Release)是大规模发布热更新资源前的必要步骤。其核心思想是:先让一小部分用户更新,验证无问题后再全量推送。
灰度策略
| 策略名称 | 实现方式 | 适用场景 |
|---|---|---|
| 按用户 ID 比例 | 根据 UID 取模决定是否进入灰度组 | 通用灰度策略 |
| 按渠道分组 | 特定渠道(如官方包、应用宝包)优先更新 | 渠道差异化运营 |
| 按地区分组 | 特定地区的玩家优先体验 | 区域性功能测试 |
| 白名单模式 | 仅指定的测试账号可以更新 | 内部 QA 测试 |
灰度实现示例
public enum UpdateChannel
{
Stable, // 稳定渠道(全量)
Beta, // 测试渠道(灰度)
Internal // 内部渠道(白名单)
}
public static class GrayscaleHelper
{
// 按用户 ID 判断是否进入灰度
public static bool IsInGrayscaleGroup(string userId, int grayscalePercent)
{
if (grayscalePercent >= 100) return true;
if (grayscalePercent <= 0) return false;
int hash = Mathf.Abs(userId.GetHashCode()) % 100;
return hash < grayscalePercent;
}
// 获取当前用户的更新渠道
public static UpdateChannel GetChannel(string userId,
bool inWhiteList, bool inBetaRegion)
{
if (inWhiteList) return UpdateChannel.Internal;
if (inBetaRegion) return UpdateChannel.Beta;
return UpdateChannel.Stable;
}
}
灰度发布的流程应当配合监控系统,实时观察灰度组的崩溃率、更新成功率、游戏时长等关键指标。一旦发现异常,立即暂停灰度并回滚。
回滚方案
无论测试多么充分,线上发布总是存在风险。因此,必须准备好回滚方案:
- 版本回滚:在 CDN 上保留至少两个历史版本,一旦新版本出现问题,将
version.txt中的latestVersion指向上一个稳定版本 - 资源回滚:AssetBundle 资源与代码分离,资源出错时可以单独回滚资源版本
- 开关控制:在服务端配置远程开关,即使客户端下载了新代码,也可以通过开关禁用有问题的功能。第 46 篇(线上监控与异常处理)会详细介绍远程开关的实现。
四、上线检查清单
在正式提审或发布前,使用以下检查清单逐项确认,可以大大降低线上事故的概率。
4.1 功能测试
| 检查项 | 检查内容 | iOS | Android |
|---|---|---|---|
| 热更新流程 | 首次安装后能否正常下载热更新资源 | ✅/❌ | ✅/❌ |
| 热更新流程 | 增量更新能否正确应用 | ✅/❌ | ✅/❌ |
| 热更新流程 | 版本回退后能否正常运行 | ✅/❌ | ✅/❌ |
| 核心玩法 | 进入场景、战斗、UI 交互是否正常 | ✅/❌ | ✅/❌ |
| 业务逻辑 | 充值、登录、排行榜等功能是否正常 | ✅/❌ | ✅/❌ |
| AOT 兼容 | 热更新代码中是否调用了未生成的泛型 | ✅/❌ | ✅/❌ |
4.2 性能测试
| 检查项 | 检查内容 | 指标要求 |
|---|---|---|
| 包体大小 | APK/AAB 包体是否在目标范围内 | iOS < 200MB,Android < 150MB |
| 热更新下载 | 首次更新下载量是否可控 | < 100MB |
| 内存占用 | 运行时峰值内存 | iOS < 1.5GB,Android < 1GB(中低端机) |
| 加载耗时 | 场景切换和热更新 dll 加载时间 | < 3秒 |
| CPU 耗时 | 热更新代码执行效率(相较于原生 AOT) | 不低于原生 90% |
| 帧率 | 游戏运行帧率 | 稳定 30fps |
4.3 兼容性测试
| 检查项 | 检查内容 |
|---|---|
| 设备覆盖 | 覆盖近两年 Top 50 主流机型(含高低配) |
| 系统版本 | iOS 12+ / Android 7+ 各系统版本测试 |
| 分辨率适配 | 刘海屏、挖孔屏、折叠屏的 UI 适配 |
| 网络环境 | 弱网(2G/3G)、WiFi 切换、断网恢复测试 |
| 后台恢复 | App 切到后台再切回,热更新状态是否正常 |
| 安装升级 | 覆盖安装(旧包直接覆盖新包)后热更新是否正常 |
4.4 安全测试
| 检查项 | 检查内容 |
|---|---|
| 代码保护 | 热更新 dll 是否已加密或混淆 |
| 资源保护 | AssetBundle 是否已加密 |
| 防篡改 | CDN 资源是否校验完整性(MD5/SHA256) |
| 防重复请求 | 更新请求是否做了防重入处理 |
| 日志脱敏 | 日志中是否泄露用户隐私或敏感信息 |
| 通信安全 | 资源下载是否为 HTTPS 协议 |
4.5 商店审核注意事项
Apple App Store 审核
提交 HybridCLR 项目到 App Store 时,需要注意以下几点:
-
代码执行策略:苹果审核团队可能会关注应用是否动态下载并执行代码。HybridCLR 的解释器模式在 iOS 上通过 AOT + Interpreter 混合运行,本质上是预编译代码,不违反苹果的审核条款。但建议在提交审核时不要在启动后立即下载热更新代码,可以在用户通过主要功能流程后再触发更新。同时,在首次提交审核时,建议在 App Review Information 备注中主动说明热更新机制,减少审核人员对动态代码执行的疑虑。
-
审核版本控制:提交审核的版本应当包含所有核心功能代码在 AOT 中,热更新仅用于修复 Bug 和微调。如果新功能依赖热更新代码,确保审核人员能够完整体验功能而无需触发热更新下载。
-
IPv6 兼容性:苹果要求应用必须支持 IPv6 纯网络环境。CDN 服务商必须提供 IPv6 解析能力,或者在客户端实现 IPv6 降级策略。建议在提交审核前使用苹果提供的 IPv6 测试工具(通过 Mac 共享 NAT64 网络)进行完整测试。
Google Play 审核
Google Play 对热更新相对宽松,但仍需注意:
-
AAB 格式要求:从 2021 年 8 月起,Google Play 要求新应用必须使用 AAB 格式发布。确保 Unity 的构建设置正确生成 AAB 文件。
-
Play Integrity API:建议接入 Play Integrity API 来验证更新请求的合法性,防止热更新资源被恶意篡改或盗用。
-
目标 API 级别:Google Play 对 targetSdkVersion 有持续更新的要求,确保 Gradle 配置中的
targetSdkVersion满足最新政策要求。
4.6 打包发布自动化
代码提交 → 单元测试 → 编译热更新 dll → 构建 AssetBundle →
生成 version.txt → 上传 CDN → 构建正式包(iOS/Android)→
自动测试 → 上传商店 / 分发平台
可以使用 Jenkins、GitHub Actions 或 GitLab CI 来实现上述流水线。将 HybridCLR 的 GenerateAll 步骤和打包命令通过命令行调用,即可实现一键发布。
GitHub Actions 配置示例
以下是一个简化的 GitHub Actions 工作流配置,演示了如何自动化 HybridCLR 的热更新资源构建与上传:
name: Build and Publish HotUpdate
on:
push:
branches: [ release/* ]
jobs:
build-hotupdate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Unity
uses: game-ci/unity-builder@v2
with:
unityVersion: 2021.3.30f1
- name: Generate HybridCLR
run: |
unity-editor -batchmode -quit -executeMethod HybridCLR.Editor.Commands.GenerateAll
- name: Build AssetBundles
run: |
unity-editor -batchmode -quit -executeMethod AssetBundlesBuilder.BuildForAll
- name: Generate Version Config
run: |
unity-editor -batchmode -quit -executeMethod VersionConfigGenerator.Generate
- name: Upload to CDN
run: |
# 使用阿里云 OSS 命令行工具上传
ossutil cp -r output/BuildData/ oss://yourgame-hotupdate/
ossutil cp output/version.txt oss://yourgame-hotupdate/
通过这样的自动化流水线,开发人员只需要将代码合并到 release 分支,系统就会自动完成热更新资源的编译和发布,大幅减少了人为操作的失误风险。在实际落地过程中,建议在流水线的关键节点设置人工确认步骤,例如在"上传 CDN"之前由测试人员确认资源包完整性、在"构建正式包"之前由项目经理确认版本号无误。自动化与人工审核的结合,才能在效率和安全之间找到最佳平衡点。
总结
本文详细介绍了 HybridCLR 项目的打包与发布全流程。在 iOS 平台上,我们通过 BuildHandler 自动化配置 Xcode 项目,从 PBXProject API 的使用到代码签名配置、ExportOptions.plist 的生成,并深入了解了 Bitcode 禁用、AOT 泛型处理等高阶适配要点。在 Android 平台上,我们配置了 Gradle 构建参数、IL2CPP 设置、多渠道打包策略以及 AAB/APK 的选型对比。对于资源发布环节,我们设计了合理的 CDN 目录结构、HTTPS 完整性校验机制和版本管理方案,并实现了基于用户分组的灰度发布机制以及多层次的回滚方案来控制更新风险。最后,通过上线检查清单,从功能测试到性能测试、从兼容性到安全性、再到商店审核注意事项和 CI/CD 自动化,确保每一环节都经过充分验证再上线。
打包发布是 HybridCLR 项目从开发走向线上的最后一公里,也是最容易出问题的一环。iOS 的签名配置、Android 的多渠道打包、CDN 的缓存策略、版本号的管理规范,任何一个环节的疏忽都可能导致线上事故。因此,建立标准化的发布流程和检查清单是保证质量的关键。建议团队在项目早期就定义好发布 SOP(标准作业程序),将本文提到的检查清单纳入其中,每次发布前后由专人逐项确认并签字归档。随着项目的迭代,这份 SOP 还应当根据实际线上事故不断更新完善,逐步覆盖更多边界情况。
至此,HybridCLR 从零搭建的实战系列已经接近尾声。回顾整个系列:从第 42 篇的 AOT 泛型原理,到第 43 篇的资源加载系统,再到第 44 篇的资源热更新系统,以及本文的打包与发布,我们已经完整地走过了从原理到落地的全过程。第 46 篇将继续探讨线上监控与异常处理,为项目的稳定运营画上句号。希望本系列的实战经验能够帮助各位开发者在实际项目中少走弯路,快速构建出稳定可靠的 HybridCLR 热更新体系。

553

被折叠的 条评论
为什么被折叠?



