Skip to content

Android / iOS 基础

Android APK 和 AAB 区别是什么?

android-apk-vs-aab

标准答案

APK 是 Android 的可安装、可执行包;AAB 是 Android 的发布包格式。 简单说:

  • APK:给设备安装用,adb install xxx.apk 可以直接装。
  • AAB:给应用商店上传用,不能直接安装;商店会根据设备生成对应 APK 或 Split APK。

Android 官方也明确说:APK 是 Android 可安装、可执行格式;AAB 只用于发布,必须由分发平台处理成 APK 后才能安装到设备上。Android Developers

底层区别

APK 通常像一个“完整安装包”,里面可能包含:

  • 多种 CPU 架构库:arm64-v8aarmeabi-v7a
  • 多种屏幕密度资源:hdpixhdpixxhdpi
  • 多语言资源
  • DEX 代码
  • Manifest
  • native so
  • assets / resources

问题是:用户设备可能只需要其中一部分。

AAB 的思路是把应用拆成模块和配置资源,然后让商店根据设备生成更合适的包。比如设备是 arm64-v8a、中文、xxhdpi,就只下这些需要的内容。官方文档里也说明,Google Play 会从 AAB 生成 base APK、feature APK、configuration APK 等 Split APK。Android Developers

对比

对比点APKAAB
本质安装包发布包
能否直接安装可以不可以
谁处理它Android 系统应用商店或 bundletool
用户最终拿到APK由 AAB 生成的 APK / Split APK
包体可能包含多设备资源可按设备裁剪
测试方便本地安装需要生成 APK 后测试
发布可用于部分渠道Google Play 新应用主要使用 AAB

Unity 项目里怎么选

开发阶段:

  • 快速真机测试,用 APK 更方便。
  • 可以直接 adb install
  • 适合本地验证功能、性能、崩溃。

发布 Google Play:

  • 通常构建 AAB。
  • Google Play 从 2021 年 8 月开始要求新应用使用 Android App Bundle 发布。Play Console Help
  • 大资源还要考虑 Play Asset Delivery、Addressables、AssetBundle 分包策略。

国内渠道:

  • 很多渠道仍然接收 APK。
  • 有些渠道对加固、渠道 SDK、签名流程有自己的要求。
  • 所以项目里经常同时维护 APK 和 AAB 构建流程。

面试加分句

TIP

“APK 是安装格式,AAB 是发布格式。AAB 的价值是让商店根据设备配置生成更小的 Split APK,减少用户下载体积。但它不能直接安装,测试时要通过 bundletool 或商店测试轨道生成可安装 APK。Unity 项目里本地调试常用 APK,上 Google Play 通常用 AAB,同时还要结合资源分包和热更新策略控制首包大小。”

iOS 包和 Android 包资源压缩有什么差异?

ios-vs-android-resource-compression

标准答案

iOS 包和 Android 包资源压缩的差异,不能只看 IPAAPK/AAB 外层容器,更要看资源本身的压缩格式。在 Unity 项目里,真正影响包体、运行内存、加载速度和 GPU 性能的,通常是贴图、音频、AssetBundle、ABI、分包策略这些内层资源。

一句话:iOS 设备更收敛,压缩格式选择相对统一;Android 设备碎片化更严重,需要兼顾 GPU 格式、ABI、渠道和分包。

底层区别

安装包外层:

  • iOS 输出 IPA,本质上也是压缩归档。
  • Android 输出 APKAAB,APK 可直接安装,AAB 由商店生成 Split APK。

但外层压缩不能替代资源压缩。比如一张贴图如果已经是 ASTC、ETC2、PVRTC 这种 GPU 压缩格式,再用 ZIP 压缩,收益通常有限。因为资源本身已经被压缩过了。

贴图压缩差异

iOS:

  • 现代 iPhone / iPad 通常优先考虑 ASTC
  • 老设备可能会考虑 PVRTC
  • 设备范围相对收敛,兼容矩阵比 Android 简单。
  • Metal 环境下格式支持更可控。

Android:

  • 设备碎片化明显。
  • 现代中高端设备通常支持 ASTC
  • 较老或低端设备可能依赖 ETC2ETC1
  • 不同 GPU、系统版本、渠道设备差异更大。
  • 如果目标机型复杂,可能要准备多套纹理包。

常见策略:

  • 高端 Android:ASTC。
  • 普通 Android:ETC2。
  • 很老的 Android:ETC1,但 ETC1 不支持 Alpha,需要额外处理透明通道。
  • iOS:优先 ASTC,特殊兼容再考虑 PVRTC。

音频压缩差异

音频上平台差异没有贴图那么强,但策略仍然要区分用途:

  • BGM:通常压缩并流式加载。
  • 短音效:可以 Decompress On Load 或驻留内存,减少播放延迟。
  • 语音:看是否频繁播放,通常压缩存储,按需加载。

Unity 里更重要的是按用途设置:

  • Load Type
  • Compression Format
  • Quality
  • 是否 Preload
  • 是否 Streaming

AssetBundle / Addressables 差异

Unity 的 AssetBundle 是平台相关的。 iOS 构建出来的 AssetBundle,不能拿给 Android 用;Android 的 Bundle 也不能给 iOS 用。

原因包括:

  • 贴图压缩格式不同。
  • Shader 编译目标不同。
  • 平台序列化数据可能不同。
  • Native 插件和资源路径策略不同。

所以热更新系统通常要维护:

c
iOS Manifest
Android Manifest
iOS AssetBundle
Android AssetBundle

Android 更复杂的点

Android 包体压缩更复杂,主要因为:

  • ABI 多:arm64-v8aarmeabi-v7a
  • 屏幕密度多:hdpi、xhdpi、xxhdpi。
  • GPU 格式支持差异大。
  • 渠道包要求不同。
  • Google Play 用 AAB,国内渠道常用 APK。
  • 大资源可能要结合 Play Asset Delivery 或自研热更新。

Unity 工程实践

我会这样处理:

  1. iOS 和 Android 分别设置 Texture Override。
  2. 贴图优先选 GPU 原生压缩格式,避免运行时解压成 RGBA32。
  3. Android 根据目标机型决定 ASTC、ETC2 或多纹理包。
  4. AssetBundle / Addressables 按平台独立构建和维护 Manifest。
  5. 首包只放启动必要资源,大资源放热更或分包。
  6. BGM、语音、短音效按使用场景设置不同压缩和加载方式。
  7. 用 Build Report 或 Addressables Analyze 检查资源占比。
  8. 真机验证内存、加载时间、发热,而不是只看包体大小。

面试加分句

NOTE

“iOS 和 Android 的资源压缩差异,本质上是平台能力和设备碎片化差异。iOS 设备更收敛,ASTC 策略更容易统一;Android 要考虑 GPU 纹理格式、ABI、屏幕密度、渠道和 AAB/Split APK。Unity 项目里不能只看安装包外层压缩,更要看贴图是否是 GPU 原生压缩、AssetBundle 是否按平台构建、首包和热更包是否拆得合理。”

Android IL2CPP 打包为什么慢?

android-il2cpp-build-slow

标准答案

Android IL2CPP 打包慢,核心原因是它不是把 C# 直接打进 APK,而是多走了一条很长的原生编译链路:

C# -> IL -> IL2CPP 生成 C++ -> Android NDK 编译 -> 链接 native so -> Gradle 打包 APK/AAB

所以慢点主要在:

  • IL2CPP 要扫描 IL、元数据、泛型、反射相关信息。
  • IL2CPP 会生成大量 C++ 文件。
  • Android NDK 要用 clang 编译这些 C++。
  • 还要链接成 libil2cpp.so
  • 如果勾选多个 ABI,会分别编译多份。
  • Release 构建还有代码裁剪、优化、符号处理、签名和压缩。

底层原理

Mono/JIT 模式下,C# IL 可以在运行时由虚拟机解释或 JIT 编译。 IL2CPP 是 AOT 方案,运行前就要把托管代码提前转成原生代码。

Android IL2CPP 大致流程是:

  1. Unity 编译 C# 脚本,生成 IL 程序集。
  2. Managed Stripping 做托管代码裁剪。
  3. IL2CPP 读取 IL 和元数据。
  4. 处理泛型、虚调用、反射元数据、类型信息。
  5. 生成 C++ 源文件。
  6. Android NDK 编译 C++。
  7. 链接成 libil2cpp.so
  8. Gradle 打包资源、native so、assets。
  9. 签名输出 APK 或 AAB。

这里最重的不是“压缩 APK”,而是C++ 生成 + C++ 编译链接

为什么 Android 特别明显

  1. 多 ABI 成本 如果同时构建 arm64-v8aarmeabi-v7a,很多原生编译和链接工作要做两遍。
  2. C++ 编译本来就慢 C++ 编译、模板展开、优化、链接都比普通 C# 编译重。
  3. 项目代码越多越慢 程序集多、泛型多、反射多、第三方 SDK 多,都会增加 IL2CPP 处理量。
  4. Release 优化更慢 Release 下可能启用更高优化、符号处理、代码裁剪,构建时间更长。
  5. 缓存失效会触发重编 改了代码、切换配置、清理 Library、升级插件、变更 IL2CPP 参数,都可能导致增量缓存失效。

Unity 工程里怎么优化

开发阶段:

  • 能用 Mono 调试时先用 Mono。
  • 只勾一个 ABI,比如只勾 ARM64
  • 用 Development Build 减少 Release 优化成本。
  • 避免每次都 Clean Build。
  • 保持 Gradle、Library、Bee 构建缓存。
  • 合理拆 Assembly Definition,减少不必要重编。
  • CI 上缓存 Unity Library、Gradle cache、构建中间产物。
  • 发布前再做完整 IL2CPP + 多 ABI + Release 构建。

发布阶段:

  • 必须确认 ABI、符号、裁剪、混淆、SDK 都正确。
  • 保留符号文件,方便崩溃堆栈还原。
  • 谨慎设置 Managed Stripping Level,避免反射代码被裁掉。
  • 对热更新方案要处理 AOT 泛型补充和 link.xml。

面试加分句

IMPORTANT

“Android IL2CPP 慢的本质是它把 C# 的托管构建问题变成了 C++ 原生构建问题。IL2CPP 要生成大量 C++,NDK 要编译和链接 native so,多 ABI 会让成本成倍增加。优化上我会区分开发构建和发布构建:开发期减少 ABI、复用缓存、避免全量构建;发布期再做完整 IL2CPP、符号和裁剪验证。”

iOS 为什么不能 JIT?

ios-no-jit-aot

标准答案

准确说,不是 iOS 技术上完全做不了 JIT,而是普通 iOS App 不能自由 JIT。原因主要是安全策略:iOS 要求可执行代码可签名、可审核、可控;而 JIT 的本质是在运行时生成机器码,再把这段内存当代码执行,这会绕开一部分代码签名和审核边界。

所以 Unity 在 iOS 上通常使用 IL2CPP + AOT,也就是提前把 C# IL 转成 C++,再编译成 iOS 原生代码,而不是运行时 JIT。

底层原理

JIT 的流程是:

IL / bytecode -> 运行时生成机器码 -> 写入内存 -> 执行这段新代码

问题在于:

  1. 代码签名 iOS 强依赖代码签名。App 里要执行的二进制代码,通常应在打包、签名、审核阶段就确定。JIT 运行时生成的机器码不是随 App 一起签名提交的代码。
  2. W^X 内存保护 JIT 需要一段内存先可写,用来写入机器码;随后又要可执行。Apple 文档也说明,JIT 会涉及可写可执行内存,而这类能力默认受限制,因为有安全风险。Apple Developer
  3. App Store 审核规则 App Store Guideline 2.5.2 要求 App 自包含,不能下载、安装或执行会引入或改变 App 功能的代码。Apple App Review Guidelines
  4. 沙箱和安全边界 如果普通 App 可以随意 JIT,就可能绕过审核、加载未审查逻辑、扩大攻击面。对游戏平台来说,这也会影响反作弊和安全边界。

Unity 里的影响

iOS 上 Unity 常用 IL2CPP AOT:

C# -> IL -> IL2CPP -> C++ -> Xcode 编译 -> iOS 原生代码

这带来几个结果:

  • 运行时不能依赖 JIT 生成原生代码。
  • System.Reflection.Emit 这类动态生成 IL/方法的能力基本不可用。
  • 某些泛型如果 AOT 阶段没生成,运行时可能出问题。
  • 反射可以用,但要注意代码裁剪和 link.xml
  • 热更新不能随意下发新的原生代码执行。
  • Lua 这类解释执行方案更符合 iOS 限制。
  • HybridCLR 等方案也要处理 AOT 泛型、元数据和平台限制,不能简单理解成“iOS 上随便 JIT”。

面试加分句

IMPORTANT

“iOS 限制 JIT,不是因为 CPU 不能执行动态生成代码,而是因为平台安全模型不希望普通 App 在运行时生成并执行未经签名和审核的新机器码。Unity 所以在 iOS 上走 IL2CPP AOT,把代码提前编译成原生二进制。代价是打包慢、泛型和反射要额外处理,但好处是符合 iOS 的代码签名和审核模型。”

移动端为什么更容易发热降频?

mobile-thermal-throttling

标准答案

移动端更容易发热降频,是因为手机的散热空间小、没有主动风扇、电池功耗受限,但游戏又是 CPU、GPU、内存、屏幕、网络长时间一起工作的高负载场景。温度持续升高后,系统会降低 CPU/GPU 频率来保护芯片、电池和手持温度,这就是降频。

一句话:移动端不是不能跑高性能,而是很难长时间维持高功耗。

底层原理

游戏运行时会持续消耗功耗:

  • CPU:逻辑、物理、AI、动画、脚本。
  • GPU:渲染、后处理、阴影、粒子、透明混合。
  • 内存带宽:贴图采样、RenderTexture 读写、深度和颜色缓冲。
  • 屏幕:高亮度、高刷新率。
  • 网络和外围:联网、语音、录屏、定位等。

这些功耗最终都会变成热。手机机身体积小,热量不容易散出去。温度接近阈值后,系统热管理会降低频率、限制功耗,结果就是:

帧时间变长 -> FPS 下降 -> 操作延迟变大 -> 画面波动

Unity 项目里常见发热点

GPU 侧:

  • 分辨率过高。
  • 大量透明物体和粒子 Overdraw。
  • 实时阴影距离过大。
  • 多后处理 Pass。
  • HDR、MSAA、Bloom、TAA、SSAO。
  • RenderTexture 频繁 Blit。
  • Shader 采样和分支过重。

CPU 侧:

  • 大量 Update
  • 大量怪物 AI。
  • 物理模拟过重。
  • 频繁 Instantiate / Destroy。
  • 日志输出过多。
  • GC Alloc 频繁。
  • 主线程资源加载或同步等待。

带宽侧:

  • 高精度 RT。
  • 多张 G-Buffer。
  • Copy Color / Copy Depth。
  • 大量全屏 Pass。
  • 高分辨率贴图频繁采样。

怎么优化

实际项目里我会先定位再动手:

  1. 用 Profiler 判断 CPU 还是 GPU 压力。
  2. 用 Frame Debugger / RenderDoc 看 DrawCall、Overdraw、RT 读写。
  3. 长时间真机跑 10 到 30 分钟,看 FPS 是否逐渐下降。
  4. 看设备温度、CPU/GPU 频率、电量消耗。
  5. 根据瓶颈降负载。

常见策略:

  • 限制帧率,比如 30 FPS 或稳定 45 FPS。
  • 动态分辨率。
  • 降低阴影距离和阴影分辨率。
  • 减少后处理和全屏 Blit。
  • 控制透明特效面积和层数。
  • LOD、Occlusion Culling、对象池。
  • AI 和寻路分帧。
  • 降低物理频率。
  • 减少 GC Alloc。
  • 使用 Unity Adaptive Performance 做动态画质调节。

面试加分句

NOTE

“移动端优化不能只看短时间峰值 FPS,要看长时间稳定性。很多机型刚启动能跑 60 FPS,但几分钟后温度上来就降频,最后掉到 40 甚至 30。我的优化思路是先用工具确认 CPU、GPU、带宽还是内存问题,再通过限帧、动态分辨率、LOD、减少后处理和 Overdraw,把功耗控制在设备热预算以内。”

机型碎片化会带来哪些问题?

mobile-device-fragmentation-issues

标准答案

机型碎片化指不同手机在硬件、系统、屏幕、GPU、驱动、权限、渠道和商店分发上差异很大。对游戏客户端来说,它会带来兼容、性能、画质、包体、测试和线上质量治理问题。

一句话:机型碎片化不是只适配屏幕,而是要管理一整套设备差异。

主要问题

  1. 性能差异大 同一个场景,高端机可能稳定 60 FPS,低端机可能掉到 25 FPS。CPU、GPU、内存、存储速度不同,会导致加载、渲染、AI、物理表现差异明显。
  2. 渲染兼容问题 Android 不同 GPU 和驱动差异大,Shader、纹理格式、后处理、精度、图形 API 都可能出问题。比如某些机型 ASTC 支持不稳定,某些驱动对 Shader 分支或精度处理有 bug。
  3. 屏幕适配问题 分辨率、宽高比、刘海屏、挖孔屏、圆角、安全区、DPI 都不同。UI 如果只按固定分辨率做,很容易出现遮挡、拉伸、按钮太小、边缘被裁。
  4. 包体和资源策略复杂 Android 有 ABI、纹理格式、屏幕密度、渠道差异。可能需要按 arm64-v8aarmeabi-v7a、ASTC、ETC2、渠道包、热更包分别处理。
  5. 系统权限和生命周期差异 不同 Android 版本和厂商 ROM 对后台、存储、通知、悬浮窗、权限弹窗、进程回收策略都不同。游戏切后台、断线重连、资源恢复都要测。
  6. 发热降频差异 有些机型短时间性能高,但几分钟后降频明显。性能测试不能只看启动后 1 分钟,要看长时间稳定帧率、温度和功耗。
  7. 测试成本高 不可能覆盖所有机型,只能建立核心真机池、自动化测试、线上监控和灰度策略。

Unity 项目里怎么应对

我会从工程上做几层处理:

  • 建立机型分档:低、中、高、旗舰。
  • 根据分档控制画质:分辨率、阴影、后处理、特效数量、帧率。
  • 根据 GPU/平台选择贴图格式:ASTC、ETC2、PVRTC。
  • UI 使用安全区适配和多比例测试。
  • 资源包按平台和渠道拆分。
  • 使用远程配置控制画质、功能开关和黑名单。
  • 做真机性能测试,不只看 Editor。
  • 线上采集 FPS、崩溃、ANR、内存、机型、系统版本。
  • 对问题机型灰度降配或临时屏蔽特效。

面试加分句

TIP

“机型碎片化的处理不能靠 if 判断堆逻辑,而要工程化治理。我会建立设备分档、资源策略、远程配置、自动化测试和线上监控。上线后通过机型维度分析 FPS、崩溃、ANR、内存和发热问题,再对具体机型做降配、黑名单或资源替换。”

刘海屏、安全区如何适配?

unity-notch-safe-area-adaptation

标准答案

刘海屏、安全区适配的核心是:背景可以铺满全屏,但重要 UI 和可点击区域要放进系统安全区内。Unity 里一般用 Screen.safeArea 拿到安全区像素矩形,再转换成 RectTransform.anchorMin / anchorMax

底层原理

Screen.safeArea 返回的是屏幕像素坐标,比如:

c
x, y, width, height

但 UGUI 的锚点是 0 到 1 的归一化坐标,所以要做转换:

c
anchorMin = safeArea.min / screenSize
anchorMax = safeArea.max / screenSize

这样 UI 根节点会自动缩进到安全区内。

Unity 实践

推荐层级:

c
Canvas
  FullScreenBackground     // 背景、遮罩、全屏特效,可以铺满屏幕
  SafeAreaPanel            // 挂 SafeAreaFitter
    TopBar
    Buttons
    HPBar
    Menu

不要把整个游戏画面都缩进安全区。 应该只让交互按钮、文本、状态栏、菜单进入安全区。

代码示例

c
using UnityEngine; // 引入 Unity 引擎命名空间
[ExecuteAlways] // 允许脚本在编辑器和运行时都执行
[RequireComponent(typeof(RectTransform))] // 要求当前对象必须有 RectTransform
public sealed class SafeAreaFitter : MonoBehaviour // 定义安全区适配组件
{ // 类开始
    private RectTransform rectTransform; // 缓存当前 UI 节点的 RectTransform
    private Rect lastSafeArea; // 记录上一次安全区,避免每帧重复设置
    private Vector2Int lastScreenSize; // 记录上一次屏幕尺寸,用于检测横竖屏变化
    private void Awake() // 对象初始化时调用
    { // Awake 开始
        Cache(); // 缓存 RectTransform 引用
        ApplySafeArea(); // 初始化时立即应用安全区
    } // Awake 结束
    private void OnEnable() // 对象启用时调用
    { // OnEnable 开始
        Cache(); // 确保 RectTransform 已缓存
        ApplySafeArea(); // 启用时重新应用安全区
    } // OnEnable 结束
    private void Update() // 每帧检测屏幕变化
    { // Update 开始
        Rect currentSafeArea = Screen.safeArea; // 获取当前系统安全区
        Vector2Int currentScreenSize = new Vector2Int(Screen.width, Screen.height); // 获取当前屏幕宽高
        if (currentSafeArea != lastSafeArea || currentScreenSize != lastScreenSize) // 判断安全区或屏幕尺寸是否变化
        { // if 开始
            ApplySafeArea(); // 变化后重新计算锚点
        } // if 结束
    } // Update 结束
    private void Cache() // 缓存组件引用
    { // Cache 开始
        if (rectTransform == null) // 如果还没有缓存 RectTransform
        { // if 开始
            rectTransform = GetComponent<RectTransform>(); // 获取当前对象上的 RectTransform
        } // if 结束
    } // Cache 结束
    private void ApplySafeArea() // 应用安全区到 UI 锚点
    { // ApplySafeArea 开始
        Cache(); // 确保 RectTransform 可用
        Rect safeArea = Screen.safeArea; // 获取系统返回的安全区域像素矩形
        Vector2 anchorMin = safeArea.position; // 取安全区左下角像素坐标
        Vector2 anchorMax = safeArea.position + safeArea.size; // 取安全区右上角像素坐标
        anchorMin.x /= Screen.width; // 把左边界转换成 0 到 1 的锚点坐标
        anchorMin.y /= Screen.height; // 把下边界转换成 0 到 1 的锚点坐标
        anchorMax.x /= Screen.width; // 把右边界转换成 0 到 1 的锚点坐标
        anchorMax.y /= Screen.height; // 把上边界转换成 0 到 1 的锚点坐标
        rectTransform.anchorMin = anchorMin; // 设置 UI 根节点最小锚点
        rectTransform.anchorMax = anchorMax; // 设置 UI 根节点最大锚点
        rectTransform.offsetMin = Vector2.zero; // 清掉左下偏移,避免额外位移
        rectTransform.offsetMax = Vector2.zero; // 清掉右上偏移,避免额外位移
        lastSafeArea = safeArea; // 记录当前安全区
        lastScreenSize = new Vector2Int(Screen.width, Screen.height); // 记录当前屏幕尺寸
    } // ApplySafeArea 结束
} // 类结束

常见坑

  1. 把整个 Canvas 都缩进安全区,导致背景黑边。
  2. 只适配竖屏,横屏切换后 UI 错位。
  3. 用固定像素偏移适配刘海,换机型就失效。
  4. 按钮贴边太近,Home 条区域容易误触。
  5. 没测挖孔屏、圆角屏、平板比例和异形屏。

面试加分句

IMPORTANT

“我不会把整个游戏画面缩进安全区,而是分层处理:背景和全屏特效铺满,核心交互 UI 放进 SafeAreaPanel。实现上用 Screen.safeArea 转成归一化 Anchor,并在横竖屏或分辨率变化时重新应用。”

后台切前台要处理什么?

unity-background-foreground-handling

标准答案

后台切前台要处理的核心是:不能假设游戏还能从上一帧无缝继续。移动端进入后台后,网络可能断、音频可能停、系统可能回收资源、时间可能跳过几分钟,回到前台时要重新校验状态。

重点处理

  1. 存档和关键状态 切后台时保存本地进度、设置、战斗临时状态,避免系统杀进程后数据丢失。
  2. 网络重连 Socket 可能断开,前台回来要重连、重新鉴权、补拉服务器状态。
  3. 时间校正 后台期间本地倒计时、体力恢复、技能 CD、活动时间不能完全信本地时间,关键逻辑要用服务器时间校正。
  4. 音频和表现恢复 回来后恢复 BGM、音效、动画、UI 状态,必要时刷新场景或重建部分资源。
  5. 防重复结算 前后台切换可能重复触发回调,奖励结算、断线重连、弹窗显示要做幂等。

Unity 回调

常用:

  • OnApplicationPause(true):进入后台或暂停。
  • OnApplicationPause(false):回到前台。
  • OnApplicationFocus(false):失去焦点。
  • OnApplicationFocus(true):重新获得焦点。

实际项目里通常两个都监听,但核心恢复逻辑要避免重复执行。

代码示例

c
using UnityEngine; // 引入 Unity 引擎命名空间
public sealed class AppLifecycleHandler : MonoBehaviour // 定义应用生命周期处理组件
{ // 类开始
    private bool wasPaused; // 记录应用是否进入过后台
    private double pauseTime; // 记录进入后台时的真实时间
    private void OnApplicationPause(bool pause) // Unity 在应用暂停或恢复时调用
    { // 回调开始
        if (pause) // 如果当前是进入后台
        { // 进入后台分支开始
            wasPaused = true; // 标记已经进入后台
            pauseTime = Time.realtimeSinceStartupAsDouble; // 记录进入后台的真实运行时间
            SaveGame(); // 保存本地关键数据
            PauseAudio(); // 暂停或降低音频播放
            StopHeartbeat(); // 停止或挂起网络心跳
        } // 进入后台分支结束
        else if (wasPaused) // 如果是从后台恢复到前台
        { // 回到前台分支开始
            wasPaused = false; // 清除后台标记
            double awaySeconds = Time.realtimeSinceStartupAsDouble - pauseTime; // 计算离开前台的时间
            ResumeAudio(); // 恢复音频播放
            ReconnectNetwork(); // 重连网络并重新鉴权
            SyncServerState(awaySeconds); // 根据离线时长和服务器状态校正数据
            RefreshUi(); // 刷新 UI,避免显示旧状态
        } // 回到前台分支结束
    } // 回调结束
    private void SaveGame() { } // 保存本地进度和设置
    private void PauseAudio() { } // 暂停背景音乐和音效
    private void StopHeartbeat() { } // 停止网络心跳或标记网络挂起
    private void ResumeAudio() { } // 恢复背景音乐和音效
    private void ReconnectNetwork() { } // 重新连接服务器
    private void SyncServerState(double awaySeconds) { } // 根据离线时长同步服务器状态
    private void RefreshUi() { } // 刷新界面显示
} // 类结束

面试加分句

WARNING

“后台切前台我会按恢复流程处理,而不是直接继续上一帧。关键是保存状态、重连网络、校验服务器时间、恢复音频 UI,并且所有恢复逻辑要幂等,避免重复触发导致重复结算或重复弹窗。”

移动端权限如何影响游戏功能?

mobile-permissions-game-impact

标准答案

移动端权限会直接决定某些游戏功能能不能用。比如麦克风影响语音聊天,相机影响扫码和 AR,相册影响头像上传和截图保存,定位影响附近玩家和 LBS 活动,通知影响活动提醒,存储影响日志、截图、资源包读写。

底层原理

Android 和 iOS 都把敏感资源放在系统权限保护后面。Android 的危险权限需要运行时申请,官方也建议在用户触发相关功能时再申请,并且拒绝后要优雅降级。Apple 也要求访问相机、麦克风、定位、相册等受保护资源前提供用途说明,由用户决定是否授权。参考:Android Runtime PermissionsApple Protected Resources

Unity 工程实践

核心原则是:权限不要和核心玩法强绑定。比如语音权限被拒绝,游戏应该还能文字聊天;通知权限被拒绝,游戏内红点和邮件系统仍然可用;相册权限被拒绝,头像可以用默认头像或游戏内头像框。

c
using UnityEngine; // 引入 Unity 引擎命名空间
#if UNITY_ANDROID // 只在 Android 平台编译权限相关代码
using UnityEngine.Android; // 引入 Unity 的 Android 权限 API
#endif // 结束 Android 条件编译

public sealed class MicPermissionExample : MonoBehaviour // 定义麦克风权限示例组件
{ // 类开始
    public void TryStartVoiceChat() // 用户点击语音聊天按钮时调用
    { // 方法开始
#if UNITY_ANDROID // Android 平台需要运行时检查危险权限
        if (Permission.HasUserAuthorizedPermission(Permission.Microphone)) // 判断麦克风权限是否已经授权
        { // 已授权分支开始
            StartVoiceChat(); // 已授权时直接开启语音聊天
        } // 已授权分支结束
        else // 未授权分支开始
        { // 未授权代码块开始
            Permission.RequestUserPermission(Permission.Microphone); // 向系统请求麦克风权限
            ShowPermissionHint(); // 提示用户授权后才能使用语音聊天
        } // 未授权代码块结束
#else // 非 Android 平台分支
        StartVoiceChat(); // 其他平台交给对应平台封装处理
#endif // 结束平台条件编译
    } // 方法结束

    private void StartVoiceChat() { } // 开启语音聊天逻辑
    private void ShowPermissionHint() { } // 显示权限说明或降级提示
} // 类结束

崩溃日志如何定位?

unity-crash-log-diagnosis

标准答案

崩溃日志定位不是“看到最后一行就改”,而是先把崩溃信息收全,再做符号化、归类、复现和验证。面试里我会按这条链路讲:崩溃栈、版本号、机型、系统、资源版本、热更版本、场景名、玩家操作路径、最近日志。

定位流程

  1. 先看影响面:崩溃率、影响用户数、集中版本、集中机型、是否热更后出现。
  2. 再看崩溃类型:C# 异常、Native 崩溃、OOM、ANR、线程问题。
  3. 做符号化:IL2CPP、iOS、Android Native 崩溃都需要对应版本的符号文件。
  4. 看栈顶和业务上下文:栈顶函数负责“哪里炸了”,上下文负责“为什么走到这里”。
  5. 本地复现:用同版本、同资源包、同机型、同操作路径验证。
  6. 修复后验证:看崩溃率是否下降,而不是只看本地不崩。

Unity 工程实践

C# 异常常见是空引用、数组越界、字典 Key 不存在、对象已 Destroy 还访问。Native 崩溃可能来自 IL2CPP、第三方 SDK、插件、图形驱动、线程非法调用。OOM 常和场景切换、贴图峰值、AssetBundle 常驻、资源重复加载有关。

c
using UnityEngine; // 引入 Unity 引擎命名空间
using System.Collections.Generic; // 引入泛型集合命名空间

public sealed class CrashContextRecorder : MonoBehaviour // 定义崩溃上下文记录组件
{ // 类开始
    private readonly Queue<string> recentLogs = new Queue<string>(); // 保存最近一段日志
    private const int MaxLogCount = 30; // 限制日志数量,避免自己制造内存压力

    private void OnEnable() // 组件启用时注册日志回调
    { // 方法开始
        Application.logMessageReceivedThreaded += OnLogReceived; // 监听主线程和子线程日志
    } // 方法结束

    private void OnDisable() // 组件禁用时取消日志回调
    { // 方法开始
        Application.logMessageReceivedThreaded -= OnLogReceived; // 避免重复注册和对象泄漏
    } // 方法结束

    private void OnLogReceived(string condition, string stackTrace, LogType type) // Unity 日志回调
    { // 方法开始
        recentLogs.Enqueue(type + ": " + condition); // 记录日志类型和内容
        while (recentLogs.Count > MaxLogCount) // 超过上限时裁剪旧日志
        { // 循环开始
            recentLogs.Dequeue(); // 移除最早的一条日志
        } // 循环结束
        if (type == LogType.Exception) // 如果捕获到 C# 异常
        { // 判断开始
            UploadCrashContext(condition, stackTrace); // 上报异常和最近上下文
        } // 判断结束
    } // 方法结束

    private void UploadCrashContext(string error, string stack) // 上报崩溃上下文
    { // 方法开始
    } // 方法结束
} // 类结束

常见坑

NOTE

只保存日志不保存符号文件,Native 栈会是一堆地址;只看栈不看版本和资源包,容易误判;只处理 C# 异常,不处理 OOM、ANR 和插件崩溃,也不完整。真正线上定位要形成闭环:收集、归类、符号化、复现、修复、验证。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

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