Skip to content

移动端发布

Android 多渠道包怎么做?

unity-android-multi-channel-package

标准答案

Android 多渠道包,就是同一套 Unity 工程,按不同应用商店或渠道生成不同安装包。差异通常包括:渠道号、登录 SDK、支付 SDK、统计 SDK、包名、图标、权限、热更新地址、资源分包策略。

常见做法

项目里一般不会手动复制工程,而是用配置驱动 + 自动化构建

  1. 维护一份渠道配置表,比如 officialhuaweixiaomioppo
  2. Gradle 用 productFlavors 或构建脚本生成不同渠道包。
  3. 把渠道号写进 AndroidManifestmeta-data,或写进 assets/channel.json
  4. Unity 启动后读取渠道号,上报给服务器。
  5. 服务器根据渠道号区分登录、支付、统计、热更新和运营活动。
  6. CI 批量打包,最后校验渠道号、签名、版本号、SDK 是否正确。

Gradle 示例

c
android { // Android Gradle 配置入口
    flavorDimensions "channel" // 定义一个名为 channel 的渠道维度
    productFlavors { // 开始定义不同渠道包
        official { // 官方渠道配置开始
            dimension "channel" // 指定官方渠道属于 channel 维度
            manifestPlaceholders = [CHANNEL_ID: "official"] // 写入官方渠道号
        } // 官方渠道配置结束
        huawei { // 华为渠道配置开始
            dimension "channel" // 指定华为渠道属于 channel 维度
            manifestPlaceholders = [CHANNEL_ID: "huawei"] // 写入华为渠道号
        } // 华为渠道配置结束
        xiaomi { // 小米渠道配置开始
            dimension "channel" // 指定小米渠道属于 channel 维度
            manifestPlaceholders = [CHANNEL_ID: "xiaomi"] // 写入小米渠道号
        } // 小米渠道配置结束
    } // 结束渠道包定义
} // 结束 Android 配置

Unity 工程里要注意

渠道 SDK 要用接口隔离,比如 ILoginServiceIPayServiceIAnalyticsService,业务层不要直接依赖某个渠道 SDK。否则接十几个渠道后,登录和支付代码会变成一堆 if channel == xxx

面试关键词

多渠道包重点不是“能打出来”,而是:自动化、渠道号可追踪、SDK 隔离、签名统一、服务端识别、热更新分流、上线前校验。最容易出事故的是渠道号错、签名错、SDK 漏接、包名不匹配、支付回调配置错误。

资源分包和首包限制怎么处理?

unity-resource-split-first-package-limit

标准答案

资源分包和首包限制,核心是:首包只放最小可运行内容,其他资源按需下载。首包里保留启动、登录、更新器、基础 UI、基础配置、首场景、公共 Shader、字体和兜底资源;后续关卡、高清贴图、语音、活动资源、非首日玩法都拆到远端包。

底层原理

首包太大,会影响下载转化、安装成功率和渠道审核;但首包太小,又会导致玩家第一次进游戏要下载太多内容。所以要平衡:首包能完成最小闭环,热更包负责扩展内容,Manifest 负责版本、Hash、大小和依赖关系。

Unity 工程实践

通常会这样拆:

  • Base:启动、登录、更新、Loading、基础配置、公共 UI。
  • Shared:公共 Shader、公共材质、通用贴图、字体、通用音效。
  • Scene_xxx:按场景或地图分包。
  • Feature_xxx:按玩法系统分包,比如副本、PVP、家园。
  • Activity_xxx:限时活动资源,活动结束后可清理。
  • Language_xxx:多语言文本和语音可按语言拆。
  • Quality_xxx:高低清资源按画质档拆。

关键处理点

IMPORTANT

依赖要去重。公共资源不要被多个 Bundle 重复打进去,否则分包后包体反而变大。可以用 Addressables Analyze 或自研依赖分析工具检查重复资源。

下载要有校验。远端 Manifest 里记录资源版本、Hash、大小、依赖列表。客户端下载后校验 Hash,失败就重试,关键资源失败不能进入玩法。

首包限制要按渠道设预算。不同商店、不同地区、不同网络环境对首包接受度不同,所以不能只按技术拆,还要看运营和渠道要求。

常见坑

分包太细会导致请求数过多,加载慢;分包太粗又会导致下载量大。活动资源忘记清理会占本地存储;Shared 包更新会牵连很多资源;Manifest 出错会导致资源版本不一致。所以必须支持灰度、回滚和版本校验。

热更新审核风险是什么?

unity-hot-update-review-risk

标准答案

热更新审核风险,核心是平台担心你绕过商店审核。资源热更一般风险较低,比如修贴图、音频、配置、活动文案;代码热更、Lua/IL 热更、远程动态代码加载风险更高,因为它可能在审核通过后改变 App 的功能和行为。

平台规则重点

iOS 风险更高。Apple App Review Guideline 2.5.2 明确要求 App 应该自包含,不能下载、安装或执行会引入或改变功能的代码;2.3.1 也要求不能有隐藏功能,新功能要在审核说明中明确展示。

参考:Apple App Review Guidelines

Google Play 也限制应用自我更新和远程可执行代码。官方政策说明,Google Play 分发的 App 不能从 Play 以外来源下载 dex、JAR、.so 等可执行代码;运行时加载解释语言也不能导致违反 Google Play 政策。

参考:Google Play Device and Network Abuse

常见审核风险

  1. 用热更暗开新玩法、新入口、新商业化功能。
  2. 绕过 App Store / Google Play 支付。
  3. 远程下发可执行代码,改变核心功能。
  4. 热更新新增权限、SDK、埋点或隐私采集。
  5. 下发违规图片、文案、活动、抽奖、涉敏内容。
  6. 审核包和线上包表现不一致。
  7. 补丁没有签名校验,被篡改后有安全风险。
  8. 热更失败后没有回滚,导致大面积崩溃。

Unity 项目里怎么规避

资源热更只做资源、配置、数值、文案、活动开关。重大功能、支付、隐私、权限、SDK 变化走正式商店更新。补丁必须有 Manifest、Hash、签名、灰度、回滚和日志监控。代码热更要限制能力边界,不能让远端脚本随意访问支付、隐私、权限和系统能力。

面试关键词

WARNING

可以这样回答:热更新不是不能做,但要守住审核边界。资源热更相对安全,代码热更要非常谨慎。所有补丁都要可校验、可灰度、可回滚,不能用热更上线审核时没展示的新功能。

iOS 审核常见问题有哪些?

unity-ios-app-review-common-issues

标准答案

iOS 审核常见问题主要集中在:App 不完整、崩溃、元数据不一致、隐私权限不清楚、IAP 不合规、隐藏功能、热更新越界、UGC 缺少审核机制、审核账号不可用。Apple 官方在提交前检查里也明确要求测试崩溃、补全元数据、提供审核账号、保证后端服务可用,并说明不明显功能和 IAP 内容:App Review Guidelines

游戏项目高频拒审点

  1. 崩溃和卡死 启动闪退、登录卡死、弱网无响应、切后台再回来异常。审核员真机跑不过去就会拒。
  2. 审核账号或服务器不可用 需要登录但没给账号,验证码收不到,服务器只对白名单开放,审核员进不了游戏。
  3. 元数据不一致 截图、商店描述、年龄分级、实际玩法不一致;宣传了未开放功能;截图里有未审核内容。
  4. IAP 问题 游戏内金币、月卡、礼包、战令、关卡解锁这类数字商品通常要走 Apple IAP。Apple 指南 3.1.1 明确要求用 IAP 解锁 App 内功能或内容。
  5. 抽卡概率未披露 Loot Box、卡池、随机虚拟物品购买前要披露获得概率,游戏很容易踩这个点。
  6. 热更新越界 资源热更通常可控,但不能用 Lua/脚本/远程代码绕审核暗开新功能。Apple 2.5.2 对下载、安装、执行会改变功能的代码非常敏感。
  7. 隐私权限问题 相机、麦克风、相册、定位、ATT 追踪说明必须清楚。隐私标签要和 SDK 实际采集一致。
  8. UGC 和聊天缺少治理 玩家昵称、头像、聊天、工会公告、评论等 UGC 要有过滤、举报、屏蔽和处理机制。Apple 指南 1.2 明确要求 UGC 有审核和举报能力。
  9. 登录和账号删除 如果支持账号创建,通常要提供账号删除入口。用了第三方登录,也要注意 Apple 对等登录能力要求。
  10. 版权和资质 IP、美术、音乐、字体、广告素材、版号、地区资质都可能被追问。

Unity 项目提审清单

IMPORTANT

提审前我会准备:可用审核账号、测试步骤、服务器环境、IAP 商品说明、隐私权限说明、热更新说明、特殊玩法说明、弱网和后台切换测试结果。上线包和审核包必须一致,不能审核时藏功能,审核后远程打开。

SDK 接入会影响哪些模块?

unity-sdk-integration-impact-modules

标准答案

NOTE

SDK 接入影响的不是一个“插件调用点”,而是一整条客户端工程链路。 面试里我会这样回答:SDK 接入通常会影响账号登录、支付、实名、防沉迷、广告、分享、推送、埋点、崩溃日志、隐私合规、多渠道打包、资源热更、服务端验签、测试灰度和线上回滚。

核心原则是:业务层不要直接调用渠道 SDK,而是封装一层 SdkFacade / ISdkAdapter。业务只认统一接口,平台差异放到适配层里。

底层原理

SDK 本质是外部平台能力接入客户端。它通常会带来:

  • 原生层依赖:Android 的 aar / jar / AndroidManifest,iOS 的 framework / plist / entitlements
  • 生命周期依赖:启动初始化、授权回调、支付回调、Activity / AppDelegate 回调
  • 异步回调:登录、支付、分享、广告播放都不是立即返回结果
  • 服务端配合:支付验单、登录 token 校验、防作弊风控不能只信客户端
  • 合规约束:隐私协议同意前,不能随便初始化采集设备信息的 SDK

Unity 工程实践

项目里我一般会这样拆:

  • SdkFacade:统一入口,业务只调用它
  • ISdkAdapter:不同渠道的适配接口
  • AndroidSdkAdapter / IOSSdkAdapter / EditorSdkAdapter:平台实现
  • AccountService:处理登录态
  • PaymentService:处理下单、验单、补单
  • AnalyticsService:统一埋点
  • BuildPipeline:处理渠道参数、签名、权限、包名
  • PrivacyService:控制隐私弹窗和 SDK 初始化时机

示例代码

c
using System; // 引入 Action 回调类型。

public enum SdkResultCode // 定义 SDK 调用结果码。
{ // 枚举开始。
    Success = 0, // 表示 SDK 调用成功。
    Cancel = 1, // 表示用户主动取消。
    Failed = 2, // 表示 SDK 调用失败。
    Timeout = 3 // 表示 SDK 调用超时。
} // 枚举结束。

public struct SdkLoginResult // 定义登录结果数据。
{ // 结构体开始。
    public SdkResultCode Code; // 保存登录结果码。
    public string Token; // 保存平台 SDK 返回的登录 token。
    public string Error; // 保存错误信息,便于日志定位。
} // 结构体结束。

public interface ISdkAdapter // 定义平台 SDK 适配接口。
{ // 接口开始。
    void Init(Action<SdkResultCode> onDone); // 初始化 SDK,并返回初始化结果。
    void Login(Action<SdkLoginResult> onDone); // 发起登录,并返回登录结果。
    void Pay(string orderId, Action<SdkResultCode> onDone); // 发起支付,并返回支付结果。
} // 接口结束。

public sealed class SdkFacade // 对业务层暴露统一 SDK 入口。
{ // 类开始。
    private readonly ISdkAdapter _adapter; // 保存当前平台的 SDK 适配器。
    private bool _loginRunning; // 标记登录流程是否正在进行,避免重复点击。
    
    public SdkFacade(ISdkAdapter adapter) // 通过构造函数注入具体平台适配器。
    { // 构造函数开始。
        _adapter = adapter; // 保存适配器引用。
    } // 构造函数结束。
    
    public void Login(Action<SdkLoginResult> onDone) // 对业务层提供统一登录方法。
    { // 登录方法开始。
        if (_loginRunning) return; // 如果正在登录,直接返回,避免重复拉起 SDK。
        _loginRunning = true; // 标记登录流程开始。
        _adapter.Login(result => // 调用具体平台 SDK,并接收异步回调。
        { // 回调开始。
            _loginRunning = false; // 收到回调后恢复登录状态。
            onDone?.Invoke(result); // 把标准化后的登录结果返回给业务层。
        }); // 回调结束。
    } // 登录方法结束。
} // 类结束。

常见坑点

  1. 支付结果不能只看客户端回调,必须服务端验单后发货。
  2. SDK 回调可能重复、乱序、超时,要做幂等处理。
  3. 隐私协议同意前,不要初始化会采集设备信息的 SDK。
  4. 多渠道包不要手工改配置,应该接入自动化构建。
  5. SDK 升级可能影响权限、包体、启动耗时、审核和崩溃率。
  6. 接入前要设计降级和回滚,避免线上 SDK 故障拖垮登录或支付。

支付 SDK 接入要注意什么?

unity-payment-sdk-integration-notes

标准答案

WARNING

支付 SDK 接入最重要的不是“能不能拉起支付”,而是保证四件事:订单可信、服务端验单、发货幂等、异常可恢复。

面试里我会这样回答:客户端负责展示商品、请求下单、拉起支付 SDK、上传支付凭证和展示结果;但是否支付成功、是否发货,必须由服务端根据平台收据、订单号、交易号校验后决定。客户端回调只能作为“支付流程结束”的信号,不能直接发货。

要注意的点

  1. 商品配置要一致:客户端商品 ID、后台商品 ID、渠道商品 ID、价格、币种要一致。
  2. 订单号必须唯一:一次购买对应一个服务端订单,不能只靠本地状态判断。
  3. 客户端不可信:金额、商品、支付成功状态都不能只信客户端。
  4. 必须服务端验单:客户端拿到 receipt / transactionId 后交给服务端校验。
  5. 发货要幂等:同一交易号只能发一次货,重复回调不能重复发奖励。
  6. 要有补单机制:断网、闪退、支付成功但未到账时,登录或进商城时主动查单。
  7. 要处理失败状态:取消、失败、超时、SDK 初始化失败都要有明确 UI 和日志。
  8. 要接入日志埋点:记录 orderId、productId、transactionId、错误码、SDK 回调时间。
  9. 要考虑合规:虚拟货币、道具、关卡等数字内容通常要遵守平台支付规则;Apple 指南 3.1.1 要求应用内解锁功能或内容通常使用 IAP,Google Play 对应用内数字商品也有支付系统要求和地区例外,具体按当前渠道政策执行。(Apple, Google Play)

Unity 工程实践

c
using System; // 引入 Action 回调类型。

public enum PayState // 定义支付订单状态。
{ // 枚举开始。
    Created, // 表示服务端订单已经创建。
    Paying, // 表示客户端已经拉起支付 SDK。
    Verifying, // 表示客户端正在等待服务端验单。
    Granted, // 表示服务端验单成功并且已经发货。
    Failed // 表示支付失败、取消或超时。
} // 枚举结束。

public sealed class PayOrder // 定义一笔支付订单。
{ // 类开始。
    public string OrderId; // 保存服务端生成的唯一订单号。
    public string ProductId; // 保存玩家购买的商品 ID。
    public string TransactionId; // 保存平台返回的交易号。
    public PayState State; // 保存当前订单状态。
} // 类结束。

public sealed class PaymentService // 定义支付服务。
{ // 类开始。
    private bool _isPaying; // 防止玩家连续点击导致重复拉起支付。
    
    public void StartPay(string productId) // 开始购买指定商品。
    { // 方法开始。
        if (_isPaying) return; // 如果已有支付流程在跑,直接拒绝重复请求。
        _isPaying = true; // 标记支付流程开始。
        RequestServerCreateOrder(productId, order => // 先请求服务端创建订单。
        { // 下单回调开始。
            order.State = PayState.Paying; // 标记订单进入支付中。
            CallPlatformPaySdk(order, receipt => // 拉起平台支付 SDK。
            { // SDK 回调开始。
                order.State = PayState.Verifying; // 标记订单进入验单中。
                UploadReceiptToServer(order, receipt, success => // 把收据上传给服务端验单。
                { // 验单回调开始。
                    order.State = success ? PayState.Granted : PayState.Failed; // 根据服务端结果更新状态。
                    _isPaying = false; // 结束支付流程,允许下一次购买。
                    RefreshPayResultUI(order); // 刷新支付结果 UI。
                }); // 验单回调结束。
            }); // SDK 回调结束。
        }); // 下单回调结束。
    } // 方法结束。
    
    private void RequestServerCreateOrder(string productId, Action<PayOrder> onDone) { } // 示例:请求服务端创建订单。
    private void CallPlatformPaySdk(PayOrder order, Action<string> onDone) { } // 示例:调用平台支付 SDK。
    private void UploadReceiptToServer(PayOrder order, string receipt, Action<bool> onDone) { } // 示例:上传收据给服务端。
    private void RefreshPayResultUI(PayOrder order) { } // 示例:刷新支付成功或失败界面。
} // 类结束。

面试加分说法

CAUTION

我不会让客户端支付回调直接发货。正确做法是:客户端拉起 SDK 后拿到交易凭证,把凭证交给服务端,服务端向平台校验,确认订单未发过货后再发奖励。这样能防止本地篡改、重复回调、断网未到账和重复发货问题。

登录 SDK 如何处理回调?

unity-login-sdk-callback-handling

标准答案

登录 SDK 的回调不能直接拿来进游戏。正确处理方式是:把不同渠道 SDK 的回调统一转换成 LoginResult,再派发回 Unity 主线程,然后检查这次回调是不是当前登录请求,最后把 SDK token 交给服务端验证,服务端通过后才进入游戏。

底层原理

登录 SDK 通常是异步回调,可能出现这些情况:

  • 成功、失败、取消、超时都要有统一出口。
  • 玩家可能重复点击登录,旧回调可能晚于新回调回来。
  • SDK 回调线程不一定适合直接操作 Unity UI。
  • 客户端拿到的 token 不能直接当游戏登录态。
  • 真正可信的是服务端校验平台 token 后返回的游戏 session。

Unity 工程实践

c
using System; // 引入 Action 回调类型。
public enum LoginCode // 定义登录结果码。
{ // 枚举开始。
    Success, // 表示平台 SDK 授权成功。
    Cancel, // 表示玩家取消登录。
    Failed, // 表示平台 SDK 登录失败。
    Timeout // 表示登录流程超时。
} // 枚举结束。
public struct LoginResult // 定义标准化登录结果。
{ // 结构体开始。
    public LoginCode Code; // 保存登录结果状态。
    public string SdkToken; // 保存平台 SDK 返回的 token。
    public string Error; // 保存错误信息,方便打日志。
    public int RequestId; // 保存本次登录请求 ID。
} // 结构体结束。
public sealed class LoginService // 封装登录 SDK 回调处理。
{ // 类开始。
    private int _currentRequestId; // 记录当前有效的登录请求 ID。
    private bool _loggingIn; // 标记当前是否正在登录。
    public void StartLogin() // 开始登录流程。
    { // 方法开始。
        if (_loggingIn) return; // 防止重复点击导致多次拉起 SDK。
        _loggingIn = true; // 标记登录流程正在进行。
        _currentRequestId++; // 生成新的登录请求 ID。
        int requestId = _currentRequestId; // 保存本次请求 ID。
        CallSdkLogin(requestId, OnSdkLoginCallback); // 拉起平台 SDK 登录。
    } // 方法结束。
    private void OnSdkLoginCallback(LoginResult result) // 处理 SDK 登录回调。
    { // 方法开始。
        RunOnMainThread(() => // 把回调逻辑切回 Unity 主线程。
        { // 主线程回调开始。
            if (result.RequestId != _currentRequestId) return; // 丢弃过期回调。
            if (result.Code != LoginCode.Success) // 判断 SDK 是否登录失败。
            { // 失败分支开始。
                _loggingIn = false; // 恢复登录按钮可点击状态。
                ShowLoginError(result.Error); // 展示失败或取消提示。
                return; // 结束失败流程。
            } // 失败分支结束。
            VerifyTokenOnServer(result.SdkToken, success => // 把 SDK token 发给服务端验证。
            { // 服务端验证回调开始。
                _loggingIn = false; // 登录流程结束。
                if (success) EnterGame(); // 验证成功后进入游戏。
                else ShowLoginError("服务器验证失败"); // 验证失败时提示玩家。
            }); // 服务端验证回调结束。
        }); // 主线程回调结束。
    } // 方法结束。
    private void CallSdkLogin(int requestId, Action<LoginResult> callback) { } // 示例:调用平台 SDK 登录。
    private void RunOnMainThread(Action action) { action(); } // 示例:派发到 Unity 主线程。
    private void VerifyTokenOnServer(string token, Action<bool> callback) { } // 示例:服务端校验 token。
    private void ShowLoginError(string error) { } // 示例:显示登录错误。
    private void EnterGame() { } // 示例:进入游戏主流程。
} // 类结束。

常见坑点

  1. 不要在 SDK 回调里直接切场景,先确认服务端验证成功。
  2. 不要信客户端 token,客户端 token 只能作为服务端验签材料。
  3. 要处理重复回调,用 requestId 或状态机过滤旧回调。
  4. 要处理取消登录,取消不是异常崩溃,UI 要恢复可点击。
  5. 要处理超时,否则玩家可能卡在“登录中”。
  6. 要记录渠道名、错误码、耗时、设备信息,方便查登录失败率。
  7. 回调里更新 UI、加载场景、访问 Unity API,统一回主线程处理。

面试关键词

标准化回调、主线程派发、状态机、requestId、防重复点击、服务端验 token、超时恢复、登录失败埋点。

防沉迷/实名认证如何接入?

unity-anti-addiction-realname-integration

标准答案

IMPORTANT

防沉迷/实名认证接入的核心是:登录后先确认玩家身份状态,未实名不能进入游戏;如果是未成年人,就按服务端策略限制登录时段、在线时长和充值行为。客户端负责展示实名 UI、提示和拦截,真正可信的实名校验和防沉迷策略应放在服务端。

国内网络游戏防沉迷规则要求严格落实实名注册登录,未实名用户不能进入游戏;官方通知还规定未成年人网络游戏服务时段限制为周五、周六、周日和法定节假日每日 20:00 至 21:00。实际项目要以发行地区、渠道和最新政策为准。

来源:国家新闻出版署通知

接入流程

  1. 玩家登录账号。
  2. 客户端请求服务端查询实名状态。
  3. 如果未实名,弹出实名 UI。
  4. 用户提交姓名和身份证信息。
  5. 服务端对接实名验证系统,返回成人、未成年或未知状态。
  6. 客户端根据服务端结果决定是否进入游戏。
  7. 未成年人进入前、在线中、充值前都要再次经过策略判断。
  8. 不满足规则时,客户端提示并阻止进入、踢下线或禁止支付。

Unity 工程实践

c
using System; // 引入 Action 回调类型。

public enum RealNameState // 定义实名状态。
{ // 枚举开始。
    Unknown, // 表示实名状态未知。
    NotVerified, // 表示玩家尚未实名。
    Adult, // 表示玩家已实名且为成年人。
    Minor, // 表示玩家已实名且为未成年人。
    Blocked // 表示当前策略禁止进入游戏。
} // 枚举结束。

public sealed class AntiAddictionService // 定义防沉迷服务。
{ // 类开始。
    private RealNameState _state; // 保存当前账号的实名和防沉迷状态。
    
    public void CheckBeforeEnterGame(Action<bool> onDone) // 进入游戏前检查是否允许。
    { // 方法开始。
        QueryStateFromServer(state => // 向服务端查询实名和防沉迷状态。
        { // 查询回调开始。
            _state = state; // 保存服务端返回的状态。
            if (state == RealNameState.NotVerified) // 判断玩家是否未实名。
            { // 未实名分支开始。
                ShowRealNamePanel(); // 弹出实名认证界面。
                onDone(false); // 暂时不允许进入游戏。
                return; // 结束当前流程。
            } // 未实名分支结束。
            if (state == RealNameState.Blocked) // 判断当前是否被防沉迷策略拦截。
            { // 拦截分支开始。
                ShowBlockedTip(); // 显示禁止进入或超时提示。
                onDone(false); // 不允许进入游戏。
                return; // 结束当前流程。
            } // 拦截分支结束。
            onDone(true); // 允许进入游戏。
        }); // 查询回调结束。
    } // 方法结束。
    
    public void CheckBeforePay(Action<bool> onDone) // 支付前检查是否允许充值。
    { // 方法开始。
        QueryPayPermissionFromServer(allowed => // 向服务端查询充值权限。
        { // 查询回调开始。
            if (!allowed) ShowPayBlockedTip(); // 如果不允许充值,显示拦截提示。
            onDone(allowed); // 把充值权限返回给商城逻辑。
        }); // 查询回调结束。
    } // 方法结束。
    
    private void QueryStateFromServer(Action<RealNameState> callback) { } // 示例:查询实名和防沉迷状态。
    private void QueryPayPermissionFromServer(Action<bool> callback) { } // 示例:查询充值权限。
    private void ShowRealNamePanel() { } // 示例:显示实名认证界面。
    private void ShowBlockedTip() { } // 示例:显示禁止游戏提示。
    private void ShowPayBlockedTip() { } // 示例:显示禁止充值提示。
} // 类结束。

常见坑点

  1. 不要在客户端本地计算年龄后直接放行。
  2. 不要相信本机时间,防沉迷时段判断应使用服务端时间。
  3. 未实名不能通过游客模式绕过。
  4. 充值入口也要接防沉迷策略,不是只限制登录。
  5. 在线过程中要有心跳检查,跨到禁玩时段要能提示并退出。
  6. 身份证、姓名属于敏感信息,要最小化采集、加密传输,客户端不要长期保存明文。
  7. 认证失败、网络失败、服务端异常时要保守处理,不要默认放行。

崩溃上报 SDK 怎么用?

unity-crash-reporting-sdk-usage

标准答案

NOTE

崩溃上报 SDK 的作用是:在游戏崩溃或发生关键异常时,自动收集堆栈、设备信息、版本信息、日志上下文,并上传到后台,方便开发按版本、机型、渠道和堆栈聚合定位问题。

面试里不能只说“接了 Bugly / Crashlytics / Sentry”。更完整的说法是:我会在启动阶段初始化崩溃 SDK,设置用户、版本、渠道、场景等上下文,运行中记录关键面包屑日志,打包时上传符号表,线上根据崩溃率和影响用户数决定修复优先级。

使用流程

  1. 游戏启动时尽早初始化 SDK。
  2. 设置版本号、渠道号、用户 ID、服务器 ID、角色 ID。
  3. 在关键流程写入上下文,比如登录、切场景、加载资源、进入战斗。
  4. 捕获 C# 未处理异常,也记录非致命异常。
  5. IL2CPP / Native 崩溃要上传符号表,否则堆栈可能只有地址。
  6. 后台按版本、机型、渠道、系统版本、堆栈聚合。
  7. 修复后发版,再观察崩溃率是否下降。

Unity 工程实践

c
using System; // 引入异常和回调相关类型。
using UnityEngine; // 引入 Unity 日志和设备信息 API。

public static class CrashReportService // 定义崩溃上报服务。
{ // 类开始。
    public static void Init(string userId, string channel) // 初始化崩溃上报。
    { // 方法开始。
        InitCrashSdk(); // 初始化具体的第三方崩溃 SDK。
        SetUserId(userId); // 设置用户 ID,方便定位具体账号问题。
        SetCustomKey("channel", channel); // 设置渠道号,方便区分不同渠道包。
        SetCustomKey("version", Application.version); // 设置应用版本号,方便按版本聚合。
        SetCustomKey("device", SystemInfo.deviceModel); // 设置设备型号,方便定位机型问题。
        Application.logMessageReceived += OnUnityLog; // 监听 Unity 日志和异常。
    } // 方法结束。
    
    public static void SetScene(string sceneName) // 设置当前场景上下文。
    { // 方法开始。
        SetCustomKey("scene", sceneName); // 把当前场景写入崩溃上下文。
        AddBreadcrumb("EnterScene:" + sceneName); // 写入一条面包屑日志。
    } // 方法结束。
    
    public static void ReportNonFatal(Exception exception) // 手动上报非致命异常。
    { // 方法开始。
        AddBreadcrumb("NonFatal:" + exception.GetType().Name); // 记录异常类型。
        UploadException(exception); // 调用 SDK 上报异常。
    } // 方法结束。
    
    private static void OnUnityLog(string condition, string stackTrace, LogType type) // 处理 Unity 日志回调。
    { // 方法开始。
        if (type == LogType.Exception) // 判断是否是异常日志。
        { // 异常分支开始。
            SetCustomKey("last_exception", condition); // 记录最后一次异常信息。
            AddBreadcrumb("UnityException"); // 写入异常面包屑。
        } // 异常分支结束。
    } // 方法结束。
    
    private static void InitCrashSdk() { } // 示例:初始化第三方 SDK。
    private static void SetUserId(string userId) { } // 示例:设置用户 ID。
    private static void SetCustomKey(string key, string value) { } // 示例:设置自定义字段。
    private static void AddBreadcrumb(string message) { } // 示例:记录关键操作面包屑。
    private static void UploadException(Exception exception) { } // 示例:手动上报异常。
} // 类结束。

关键注意点

  1. 只接 SDK 不够,必须上传符号表。
  2. 线上包的版本号、渠道号、资源版本要能对应到构建产物。
  3. 日志不能上传密码、token、身份证、手机号等敏感明文。
  4. 面包屑日志要短,只记录关键路径,避免上报包过大。
  5. 非致命异常要限流,否则可能刷爆后台。
  6. 崩溃修复要看数据闭环,比如 crash free rate、影响用户数、版本趋势。
  7. IL2CPP 下 Native 崩溃经常需要符号化后才能看懂真实函数名。

线上灰度发布怎么做?

unity-online-gray-release-process

标准答案

线上灰度发布就是:新版本、新资源或新功能不要一次性全量开放,而是先放给一小部分用户,观察崩溃率、登录成功率、支付、资源下载、卡顿等指标,稳定后再逐步扩大;一旦异常,要能快速关闭开关或回滚版本。

在游戏客户端里,灰度通常不是单独发一个包,而是组合使用:应用商店分阶段发布、资源 Manifest 灰度、远程配置灰度、功能开关灰度、服务端接口灰度。

完整流程

  1. 先准备可回滚产物:客户端包、资源包、配置、服务端版本。
  2. 定义灰度人群:白名单、渠道、地区、设备、UID 百分比。
  3. 用稳定分桶:比如 uidHash % 100 < grayPercent,不要每次随机。
  4. 客户端启动时拉取灰度策略。
  5. 根据策略决定是否启用新功能、下载新资源、连接新接口。
  6. 观察指标:崩溃率、登录失败率、下载失败率、卡顿、支付转化。
  7. 指标稳定后扩大比例:1% -> 5% -> 20% -> 50% -> 全量。
  8. 指标异常时熔断:关功能开关、回滚 Manifest、切回旧服务端策略。

Unity 工程实践

c
using System; // 引入 Math 和基础类型。
public sealed class GrayReleaseService // 定义灰度发布判断服务。
{ // 类开始。
    private readonly int _grayPercent; // 保存灰度比例,例如 5 表示 5% 用户。
    public GrayReleaseService(int grayPercent) // 通过构造函数传入灰度比例。
    { // 构造函数开始。
        _grayPercent = Math.Clamp(grayPercent, 0, 100); // 限制灰度比例必须在 0 到 100 之间。
    } // 构造函数结束。
    public bool IsInGrayGroup(string userId) // 判断当前用户是否命中灰度。
    { // 方法开始。
        if (string.IsNullOrEmpty(userId)) return false; // 没有用户 ID 时不进入灰度。
        int hash = Math.Abs(userId.GetHashCode()); // 根据用户 ID 计算稳定哈希值。
        int bucket = hash % 100; // 把用户稳定分到 0 到 99 的桶里。
        return bucket < _grayPercent; // 判断桶编号是否落在灰度比例内。
    } // 方法结束。
    public bool ShouldEnableFeature(string userId, bool remoteSwitch) // 判断功能是否开启。
    { // 方法开始。
        if (!remoteSwitch) return false; // 远程总开关关闭时直接禁用功能。
        return IsInGrayGroup(userId); // 总开关开启时再判断用户是否命中灰度。
    } // 方法结束。
} // 类结束。

面试重点

灰度发布一定要讲“闭环”: 不是发出去就完了,而是要有分桶、开关、监控、告警、回滚、复盘。

项目里我会把灰度能力拆成几层:

  • 配置层:远程配置控制灰度比例、白名单、功能开关。
  • 资源层:Manifest 指向不同资源版本,异常时切回旧 Manifest。
  • 业务层:入口、活动、技能、玩法用 feature flag 控制。
  • 服务端层:协议和数据结构保持兼容,新老客户端都能跑。
  • 监控层:按版本、渠道、机型、灰度组统计指标。
  • 回滚层:保留旧包、旧资源、旧配置,支持快速恢复。

常见坑点

TIP

  1. 每次随机灰度用户,导致玩家今天有功能、明天没功能。
  2. 只灰度客户端,不考虑服务端协议兼容。
  3. 新资源发布后立刻删除旧资源,导致回滚失败。
  4. 没有 kill switch,线上出问题只能重新发包。
  5. 只看崩溃,不看登录、支付、下载失败和卡顿。
  6. 灰度组和对照组不区分,无法证明问题是不是新版本造成的。
  7. 代码热更和动态下发逻辑要注意平台审核边界,不能绕过平台规则。

一句话总结

线上灰度发布 = 稳定分流 + 可版本化产物 + 实时监控 + 快速回滚。 面试里把这四个点讲清楚,就比单纯说“先发 10% 用户”扎实很多。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0