多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity跨平台游戏Android构建与真机调试全攻略

Unity跨平台游戏Android构建与真机调试全攻略 做跨平台游戏最烦躁的环节我闭着眼都能回答Android平台构建。编辑器里跑得行云流水的游戏打包上真机以后就是另一副面孔纹理发花、中文变豆腐块、开机动画都看不完就闪退回桌面。之前我整理跨平台项目的部署笔记时第16章恰好是“Android平台构建”专题里面记录的就是《暗黑王朝》这个项目的完整实践。它是一个典型的暗黑风格ARPG双端同发国内安卓机型从骁龙8系旗舰到千元机都得覆盖整个工程从Unity构建到跨平台部署再到真机调试踩过的坑、试过的方案都在这一章里。内容以Unity为主但构建思路和调试方法放到Cocos、Flutter这类跨平台项目上也通用想照着做的可以先收藏再慢慢看。1. 项目画像与构建方案选型1.1 《暗黑王朝》到底是个什么项目先交代清楚项目本体后面的构建和调试才能对上号。《暗黑王朝》是一款暗黑风格的俯视角ARPG玩法类似“刷装备加走位打怪”美术上用了大量粒子特效和动态光照角色模型精度放在手游里属于中上水平场景里实时阴影、体积雾这些重头效果都是开着的。双端同发的目标下Android和iOS要共享同一套美术资产和核心代码所以从立项第一天起引擎选型和构建管线就是围绕“跨平台”这三个字来定的。这个项目的现实约束很具体发布地区包括国内和海外国内安卓覆盖华为、小米、OPPO、vivo、荣耀、一加等主流品牌系统版本从Android 8到Android 14都有活跃用户海外以Google Play为主要求支持Android 10以上就够。设备性能跨度非常大从骁龙8Gen2跑满帧率的目标机到骁龙695这类入门机都得保证能进游戏、能打主线。这意味着纹理压缩、内存占用、Shader复杂度这些事从构建阶段就要严格按最低配置去卡而不是等上真机再返工。1.2 为什么引擎选型选了Unity选Unity而不是Unreal或自研引擎当时的理由放在今天仍然成立。暗黑ARPG卖点是战斗反馈和特效表现Unity的URP渲染管线能很好地平衡画面效果和移动端性能Unreal的Nanite和Lumen确实惊艳但那是给PC和主机准备的放在手机上有包体和功耗两座大山而且团队对C技术栈的掌控力不如C#。Cocos Creator上手快、包体小但3D渲染管线相对薄弱做2D卡牌或者轻量3D没问题对付这种重特效ARPG会吃力。另一个决定性因素是生态。《暗黑王朝》需要用到登录、支付、崩溃监控、热更新、实时语音这些基础能力Unity的Android插件生态和厂商适配案例都比Cocos全遇到问题搜一圈基本能找到答案。说白了跨平台项目最怕的不是引擎功能不够而是某个平台的坑没人帮你趟过Unity在Android侧的“人肉文档”厚度是目前最合适的。1.3 Android构建产物的三种路线开始处理构建前先要把“最终产出什么”这个目标定清楚。Android侧的交付产物一般有三种形态APK、AAB、APKAssetBundle分离包。我们的结论是国内渠道全部给APKGoogle Play给AAB后续所有版本更新走AssetBundle热更把APK体积压缩到安装包本身只承载引擎和首包资源。APK的好处是直接可装、渠道侧载方便国内应用商店普遍不接受AAB所以它是国内市场绕不开的形态。AAB是Google Play从2021年8月起对所有新应用强制的要求它会把资源按设备配置拆分成base包和配置包由商店在下载时按机型生成最终APK好处是安装包体积显著变小代价是不能再通过商店渠道做“按cpu架构分别出包”这种传统操作。AssetBundle分离包则解决迭代问题游戏跑起来以后只下增量资源不需要重新安装整个APK。三种形态不是互斥的反而要配合着用这也是后面构建配置里要同时处理好Gradle打包和Addressables分组的原因。2. Android平台构建配置全流程2.1 开发环境SDK、NDK、JDK版本匹配构建环境这件事一句话总结版本不匹配是Android构建失败的最大来源。我们用的是Unity 2021.3 LTS官方推荐的配套是JDK 11、Android SDK 30、NDK r21d。Unity Hub在安装Android模块时会自带OpenJDK和SDK不用自己额外折腾Java环境但NDK需要单独勾选很多人漏掉这一步结果构建时IL2CPP怎么都编不过出libil2cpp.so。JDK、SDK、NDK三者的版本必须跟Unity版本要求对齐。Unity是C#脚本但它最终会通过IL2CPP编译成C再编成平台二进制所以NDK直接决定native层的产出物。如果装了自带NDK之外的其他版本构建时会出现各种莫名其妙的“NDK not configured”或者链接错误。我的建议是直接使用Unity Hub里勾选的配套版本不要手动去Android Studio里另配一套省下的时间远大于切换版本带来的好处。刚接手项目的同学第一步先看Editor.log里记录的构建工具链版本再对照官方兼容表能省一晚上排查时间。2.2 Player Settings里的门道构建配置真正的主战场在Player Settings这一节全是细节包名Package Name分渠道定国内统一用com.darkdynasty.cn海外用com.darkdynasty.gp。这里要注意包名一旦上架就不能改改一次等于换了个新应用。版本号Unity的bundleVersion对应Android的versionNamebundleVersionCode对应versionCode。versionCode是Android用来判断升级的唯一整数渠道包重复或回退都会在应用商店那边卡住建议用构建脚本自动递增。Min SDK与Target SDKMin SDK低决定能覆盖多少老机型我们定在21Android 5.0再低就没有实际用户了Target SDK要跟着Google Play的合规要求走2023年以后新上架应用必须target Android 13API 33以上。Target SDK不是越高越好定得过高会影响低版本机型上的兼容行为要按商店要求和实际测试结果平衡。Scripting BackendMono在Android上已经边缘化IL2CPP是唯一选择。IL2CPP把C#转成C再编译启动速度和内存布局更可控还能规避Mono的JIT限制。代价是构建时间明显变长首次构建可能十几分钟起步后面有增量会好一些。目标架构ARM64是必须保留的armeabi-v7a可以根据用户设备分布决定要不要保留2024年还在活跃的32位设备已经很少了保留它会白白增加包体。图形API优先用VulkanOpenGLES3作为fallback。URP在Vulkan下的表现更稳定某些厂商GPU驱动对OpenGLES的兼容性问题明显更多。Managed Stripping Level建议用Low或Medium太高会把反射用到的代码裁掉运行期才爆空引用排查起来相当痛苦。这些参数没有统一的最优解但有一条原则所有改动要以“最低配设备上能跑”为准而不是以开发机为准。2.3 资源打包与UI适配的细节构建不只是点一下Build按钮资源怎么打包、UI怎么适配直接决定用户打开游戏后的第一印象。纹理压缩是Android构建最容易翻车的一环。桌面平台常用的DXT格式在Android上大批GPU不支持我们统一用ASTC这也是主流安卓机芯默认支持的格式。URP的渲染管线、Shader里的贴图采样只要纹理格式乱了最典型的表现就是模型变紫、贴图糊成一片或者直接闪退。美术在出图时就要按ASTC 6x6到8x8的质量等级出进包时再按平台覆盖一次确保不会发生“美术本地看着好、打包后全是马赛克”的惨案。字体问题同样要在构建阶段提前处理。中文动态字体会在运行时依赖系统字形渲染不同厂商ROM对中文字体的支持不一样轻则个别字变成方框重则渲染线程卡顿。我们最终改为预烘焙字体图集把常用中文字符集覆盖5000常用字在构建时生成位图字虽然包体增加了几兆但换来的是所有机型渲染一致这个交换在我个人看来非常值。UI适配还得分清几种屏。主流Android设备从16:9到20:9、刘海屏、挖孔屏都有Unity的Screen.safeArea接口必须在启动时读取并应用到底层UI容器上否则刘海区域会遮住按钮。我们做了一个三级的UI安全区方案顶栏避开系统状态栏和挖孔区域侧边按钮距离安全区边界至少24dp全屏过场图允许延伸到挖孔区之外。构建配置里再配合Android Adaptive Icon把桌面图标和通知栏小图标分开输出避免桌面图标被裁成奇怪形状。2.4 构建过程中的常见坑这一节几乎是从泥坑里爬出来的记录。Gradle构建超时或失败90%是网络问题或依赖冲突国内开发者把maven仓库换成阿里云镜像基本能解决一半另一半是第三方SDK版本跟Unity Gradle模板冲突表现是“Duplicate class”错误处理思路是检查Assets/Plugins/Android下的依赖清单把重复的support库或androidx库排除掉。另一个高频坑是构建内存不足。Unity会通过Gradle调用Java编译默认堆内存可能不够项目变大的时候经常出现“OutOfMemoryError: Java heap space”。在Assets/Plugins/Android/mainTemplate.gradle里调大org.gradle.jvmargs即可比如设成-Xmx4096m。如果项目里同时挂了多个渠道SDKliberation包很容易超过2GB的dex限制需要开启MultiDex并合理配置minSdkVersion否则打出来的包安装到低版本设备上直接报“Unable to start activity”。第三类坑跟热更新有关。AssetBundle文件名、CRC校验、manifest版本号对不上就会出现下载进度条卡在99%然后闪退的现象。构建时机上我们强制要求AB构建和代码版本同步打Tag构建脚本里校验两者一致从根上避免了“资源更新了、逻辑没更新”的错位问题。3. 跨平台部署的工程化实践3.1 代码与资源的平台隔离跨平台工程里最忌讳把平台相关逻辑散落在各处。我们的代码约定很简单核心逻辑不直接引用任何平台API所有平台差异全部收敛到Facade层。比如保存存档业务代码只调SaveManager.Save()内部再用UNITY_ANDROID、UNITY_IOS、UNITY_EDITOR这些宏分别走不同实现。这样Android构建时只要看SaveManager.Android.cs这一个文件就够了排查问题范围小了很多。资源的平台隔离同样靠目录约定Assets/Plugins/Android放AAR和Java源码Assets/Plugins/iOS放OC和Swift封装Assets/StreamingAssets放跨平台共享的原始资源。Android平台要注意StreamingAssets在真机上的读取路径不是File路径Unity会把它映射到jar包内部直接File.Exists会返回false必须用UnityWebRequest或者Application.streamingAssetsPath加jar协议处理这一步是最容易被新手忽略的。如果产品线往后要扩展到Android TV或车机Android Automotive OS这套目录约定和宏隔离的方式同样适用只要能保证平台差异化代码收敛在一个接入层新平台的适配成本就会很低。3.2 Android专属目录与文件读写调试Android游戏绕不开应用专属目录的概念。构建出来的游戏安装后数据目录在 /storage/emulated/0/Android/data/包名/ 下面里面又分成files、cache、databases这些子目录。Unity的Application.persistentDataPath在Android上就指向这个目录下的files子目录存档、日志、下载的AB资源都放这里。这个目录的好处是不需要申请存储权限卸载应用时自动清除不会污染用户存储空间。但麻烦也跟着来Android 11以后系统限制第三方直接访问 /Android/data 路径哪怕是开发者工具adb想要查看这个目录在部分机型上也会被弹窗拦着。调试时我们最常用的两条路一条是adb shell run-as 包名可以绕过文件管理器直接进入应用私有目录另一条是在应用内部实现一个日志导出接口把日志文件复制到明确的公共下载目录再通过USB拉出来。项目里我们还保留了一个Debug面板输入特定指令就能把当前persistentDataPath的完整内容打包下载这个功能在解决“用户说存档丢了”这类问题上帮了大忙。3.3 第三方SDK的Android适配跨平台部署里最磨人的是接第三方SDK尤其是国内Android。我们在《暗黑王朝》里接了登录、支付、推送、统计、崩溃监控、热更新这些基础组件前前后后踩过的坑可以单开一篇文章这里挑三个最有代表性的第一个是AndroidManifest合并冲突。每个SDK都会带自己的Activity、Service、权限声明合包时经常报“Attribute applicationtheme value(...) from AndroidManifest.xml”冲突。处理方案是建立一份渠道清单覆盖模板在mainTemplate.gradle或者Unity的gradleTemplate.properties里通过tools:replace指定用我们的配置覆盖SDK默认值。第二个是运行权限的适配节奏。Android 6开始运行时权限要动态申请Android 13又把通知权限单拎出来变成运行时权限Android 14还给部分权限加了“仅本次使用”的选项。如果只做Android 10那一套权限申请逻辑用户升级系统后通知和推送全静默失效排查起来特别隐蔽。我们的原则是把所有运行时权限统一封装成一个PermissionHelper按Build.VERSION.SDK_INT走分支新系统出新权限就往里加不让业务代码直接碰权限API。第三个是微信支付、QQ登录这类SDK的回调Intent配置。Android上的第三方登录会把结果通过Intent回调到特定的Activity如果这个Activity没有配置正确的launchMode会出现登录成功但回调不执行、用户永远卡在授权页的灵异现象。这种问题看Unity的日志看不出来必须抓logcat的ActivityManager日志才能发现是StartActivityForResult没有收到返回。至于文本复制、分享这类看似不起眼的小功能Android也有一份剪贴板权限规则配错了表现就是复制没反应、分享面板空白建议在接入时就写好自动化冒烟用例。3.4 一键构建与CI/CD手动点Build按钮打渠道包是上线前最浪费时间的事。我们把构建流程全部脚本化之后一次出5个渠道包的时间从半天缩到了半小时。Unity支持命令行批处理核心命令是Unity -batchmode -quit -projectPath /path/to/project -executeMethod BuildScript.BuildAndroid -buildTarget Android -logFile build.log函数里用BuildPipeline.BuildPlayer去执行Android构建再配合自定义参数切换渠道、版本号、资源分组。现在也有人会用平台自带的智能体辅助生成这类构建脚本跟用Python手写构建脚本相比智能体能快速搭出骨架但在Android这种构建细节极多、需要严格版本控制的环境里构建脚本的可维护性比生成速度重要得多最终还是要靠人工梳理的命令脚本保证可复现。脚本的好处不只是快还在于可重复。CI平台比如Jenkins或者云构建机器上拉下代码后自动跑脚本任何一次构建都能追溯到唯一的版本号、代码Commit号、AB资源Hash。这跟本地手点按钮最大的区别是“可复现”用户反馈某个包有问题时你能准确说出这个包是什么代码、什么资源打出来的而不是凭记忆猜。构建脚本里还有个容易被忽略的细节构建前要清理旧的AssetBundle缓存和临时目录否则增量构建会把旧资源残留进去表现就是线上包比本地包大了几十兆查起来费时又费眼。4. 真机调试与日志分析实录4.1 连接真机USB调试与无线调试构建出包只是第一步真机调试才是每天要面对的日常。先解决设备连接Android手机默认不开放adb通道要先在设置里找到“版本号”连点7次开启开发者模式再打开USB调试。这里有个高频疑问adb驱动需要USB调试模式吗答案是必须开USB调试驱动只是让电脑识别到设备adb权限完全由手机端的调试开关控制。不开调试模式adb devices下面只会看到unauthorized或者干脆一片空白。不同品牌还有额外开关。小米MIUI在“开发者选项”里要同时打开“USB安装”和“USB调试安全设置”否则安装测试包时会弹危险操作确认华为HarmonyOS/EMUI要在“仅充电模式下允许ADB调试”开启后才能在一次次的确认弹窗中把设备状态变正常一加和OPPO相对省心但部分认证线材会导致设备反复断开重连平板机型上的入口位置也会不一样。我们测试组有个不成文的规矩固定用几根原装线不混用杂牌线省掉了大量“为什么设备不停重连”的排查时间。Android 11以后推荐优先用无线调试。前提是手机和电脑在同一局域网进入开发者选项的“无线调试”用配对码先配对再执行adb pair 192.168.1.20:37000 adb connect 192.168.1.20:37300这种方式的优势是真机随手拿不用每次物理插线但要注意Wi-Fi信号弱的时候丢包严重日志会出现大量断流。我们通常USB调试用来首次安装、传输大文件无线调试用来日常打日志和看性能两条路换着来。4.2 Unity日志与logcat的组合拳Android调试绕不开logcatUnity项目也不例外。Unity引擎会把Debug.Log输出到logcat的Unity标签下基础命令是adb logcat -s Unity只看这个标签就够日常定位。但崩溃日志和系统级报错不总在Unity标签下我建议先清空缓存再复现复现完立刻全量抓取adb logcat -c adb logcat -d *:E error.log按时间线从下往上翻把“用户操作-系统报错-应用闪退”串起来看。这里要提一个对比有人说调试native层为什么不用gdb那一套其实Android游戏开发里gdb只是偶尔用更多时候是跟嵌入式、硬件调试完全不同的玩法日常主力是logcat加Unity Profiler。C#逻辑的异常、SDK的Java异常、native层的crash都会在logcat里有不同特征的输出先学会读日志比会开一堆工具重要得多。日志永久化也很关键。真机连着电脑调试时Unity自带的日志窗在Android真机上不显示要在代码里挂Application.logMessageReceived把日志同时打到文件和控制台这个思路跟PC端“调试信息保存到日志文档同时打印显示”的做法一致但Android上文件路径要写到persistentDataPath否则没有权限。我们线上版本就靠这套落盘日志用户报障时在工单里附带日志文件解决率提升了一大截。4.3 崩溃定位三类崩溃的分诊我把自己归成三类Java崩溃、Native崩溃、脚本层崩溃。它们的特点和处理方式完全不同。Java崩溃一般会弹对话框提示“应用已停止”logcat里有明确的Exception堆栈。常见来源是SDK回调空指针、启动Activity没注册、广播接收器异常。处理方式是抓完整堆栈找到我们自己的业务代码或SDK版本问题基本半小时内能定位。Native崩溃的表现是直接闪退无提示logcat里会出现SIGSEGV、SIGABRT有时会带“Fatal signal”字样堆栈里能看到libil2cpp.so、libunity.so。这类问题要么是第三方C库踩内存要么是IL2CPP层自身的Bug。定位需要符号表构建时保留symbols.zip用Unity的addr2line工具把地址翻译成函数名。也有人用Android Studio的sdk native工具链做同样的事但Unity项目里还是以Unity提供的符号解析为准。脚本层崩溃是“假崩溃”游戏没退出但是逻辑卡死或空引用Unity日志里能看到NullReferenceException。这类问题往往跟热更新资源缺失、配置表没打到包有关。处理方式是看日志里有没有Assets/...路径的堆栈指向再结合地址和AB包版本一起分析。三种崩溃的排查日志我都会先用脚本做一次预处理把时间线、包名、堆栈摘要自动抽出来再人工看细节效率能提升不少。4.4 性能调试不能只看Profiler构建和崩溃问题解决以后性能调试才是长期战役。Unity Profiler附在编辑器和真机上都能跑但有一个普遍误区只看Profiler的CPU数据忽略真机上的实际功耗和发热。我们的做法分三步第一先用Profiler看主线程耗时和渲染线程耗时找到瓶颈是逻辑还是渲染。暗黑类ARPG怪多的时候粒子特效和骨骼动画可能吃掉一半以上的渲染帧预算。第二用GPU Profiler看片元着色器复杂度和overdraw。URP合批之后透明特效的overdraw会特别严重需要靠降低特效分辨率、切换成半分辨率渲染来缓解。这些调整不重新构建引擎只改渲染组件的质量等级即可生效方便真机快速试。第三务必在最低档位设备上验证。我们测试库里常驻一台骁龙695的手机帧率不满30帧就视为不达标的版本开发机帧率再高都不算数。Android 11以后还有Sustained Performance Mode可以让CPU在长时间高负载下主动降频来保护温度和续航这个模式一开帧率曲线会有一段明显的阶梯下降如果游戏本身没做好负载控制玩家过一会儿就会经历体感上的“变卡”这个问题靠Profiler是看不出来的只有长时间游玩测试能发现。5. 常见问题与排查技巧速查5.1 构建期问题速查现象常见原因处理建议登录Gradle构建超时依赖下载网络不畅切换国内镜像仓库调整Gradle超时配置IL2CPP编译失败NDK版本不匹配回退到Unity配套的NDK版本构建出现Duplicate class第三方SDK依赖重复在Gradle配置里排除重复的support/androidx依赖包体比预期大很多未清理旧AB缓存构建前清空临时目录禁用增量残留桌面图标被裁切未配置Adaptive Icon用自适应图标模板补充前景层和背景层资源5.2 运行期问题速查现象常见原因处理建议安装提示“安装失败”签名不一致或版本号低于已有包检查keystore签名上调versionCode打开黑屏后闪退Texture格式不兼容或Shader不支持统一ASTC纹理格式检查GPU降级回退日志文件写不进去未使用应用私有目录写入路径改为persistentDataPathAndroid 13收不到通知通知权限未运行时申请升级到最新的通知权限申请分支数据目录看不到文件Android 11对/data目录限制使用adb shell run-as包名访问特效多时掉帧overdraw严重降低特效分辨率启用半分辨率渲染5.3 跨平台部署的三个独家心法第一个心法维护一份“平台差异清单”。从立项第一天起任何一次“Android能跑iOS不能跑”或反之的经验都记进这份文档。它有两大用途一是新同学上手不用重新踩坑二是构建方案选型时有据可查。这份清单里至少要包含路径差异、权限差异、生命周期差异、输入差异、SDK回调差异这几类每一条都要写明“现象-原因-解决时间”长期积累下来它就是项目里最值钱的wiki。第二个心法所有调试手段最终都要能在命令行里跑。不管是adb logcat、安装包、拉日志还是查ANR堆栈只要人肉操作的步骤一多必然出错。我们把常用的调试命令封装成shell脚本一个命令就能完成“连上设备、安装最新包、清日志、复现、抓日志、导出崩溃堆栈”整套流程极大地释放了测试人员的精力。第三个心法不要在开发机上验证Android包。开发机性能太好会把很多问题掩盖掉。我们坚持用测试库里最老的设备作为“红线机”构建出来的包先在它上面跑一遍能跑过基本问题不大。注意这不是“性能测试”而是“兼容性门槛”它过滤掉的是最常见的那批低级问题。最后再分享一个我自己的体会跨平台部署这件事技术含量最高的阶段是“把它构建出来”但真正决定项目成败的是“把它迭代下去”。构建脚本、日志体系、崩溃处理这套工程基础打牢了后面每次版本更新都是顺手的事。而如果这些基础一开始就没搭好那每次发版都是新的煎熬。《暗黑王朝》走到今天回头看最值的投资不是某个炫技的Shader而是那套稳定到让人忘记它存在的构建管线。
返回列表