做 AR 项目到现在,我最大的体会是:新手期最怕的不是算法也不是模型,而是好不容易把功能跑通,到了打包发布环节,Android 和 iOS 各给你来一闷棍。这篇记录的完整项目就是基于 Unity + easyAR 开发的一个图片识别 AR 应用,目标很朴素——Android 和 iOS 双端发布,最终还要把识别和互动过程通过 easyAR 自带的录屏功能录制下来。结果从选 SDK 版本、配 License Key,到 Android 混淆、iOS 权限描述,再到录屏黑屏、无声、文件损坏,问题清单拉出来一大串。如果你正准备用 easyAR 做跨平台 AR 项目,或是需要把“录屏”这种看似简单的功能加进去,这篇应该能帮你少走很多弯路。
文中涉及截图的部分,我会用文字说明对应面板和菜单位置,各位对照自己工程里的窗口找一下就行。
1. 先别急着写代码:easyAR 选型与激活阶段的三笔账
很多教程一上来就讲识别、跟踪、坐标转换,但实际项目的第一个坑往往出现在开始动工之前。我用一句话总结:easyAR 项目 80% 的发布问题,在选 SDK 版本、填 License Key 和规划资源加载方式时就已经埋下了。
1.1 版本选型:新 SDK 和老教程怎么取舍
easyAR 的 SDK 更新节奏不算慢,早期 3.x 的教程、博客、视频在搜索时能翻出一大堆,而新版 SDK 的 API 和授权模式都变了不少。我见过不少开发者照着 3.x 的旧帖子去用新版 SDK,结果发现接口对不上、授权不通过、渲染管线冲突,最后绕了一大圈才明白是版本代差问题。
我的建议很直接:新项目默认用厂商当前主推的 Sense 版本和配套的 Unity 包,不要因为“网上的教程多”去选旧版。同时,Unity 版本也不要太激进,先对照 SDK 官方文档里写明支持的 Unity 版本范围。比如某个 SDK 版本明确支持 Unity 2021/2022,那就别为了尝鲜用 Unity 2023 的预览版,否则构建时出现的渲染底层变化会把你的排查方向带偏。
实测下来,Unity 2021 LTS + 当前主推 easyAR 版本是我试过最稳的组合。LTS 版本的工具链、IL2CPP 行为和 Gradle 版本都比较成熟,不会因为 Unity 本身的问题干扰你判断 AR 功能的 Bug。
1.2 License Key:包名、激活与缓存机制
easyAR 的授权机制是我在很多项目里见过最容易被忽视的环节。申请 Key 的时候,你要填应用的包名,这个包名必须和你最终发布到商店的包名完全一致。Android 看 applicationId,iOS 看 Bundle Identifier。很多人前期用默认包名,后期要上架才改,一改 Key 就失效,运行时报错提示不匹配,还得重新申请等审核,白白耽误时间。
激活方式也值得留意。首次联网激活成功之后,SDK 会在本地缓存激活信息,后续可以离线使用。但如果在没有网络的开发环境里首次初始化,就会一直卡在激活失败状态。我的习惯是:工程建好后的第一件事,把正式包名先定下来,然后在联网状态跑一次空场景完成激活,后面就踏实了。
1.3 素材加载方式:StreamingAssets 还是 AssetBundle
AR 项目里不只是识别图,还有模型、视频、音频、配置表。素材放哪里会直接影响到双端发布和录屏体验。我一开始图省事,所有东西都丢 StreamingAssets,简单直接,但 APK 和 IPA 的体积一下就上去了,而且需要网络下载更新内容的场景也没法做。
后来换成了本地 StreamingAssets 放一部分基础资源和识别图,远程加载放 AssetBundle 或 Addressables。这里踩过的坑是:AssetBundle 加载时机没有处理好,导致用户相机已经识别到图,模型还没加载出来,画面上出现一段空窗期。正确做法是在进入 AR 场景前预热资源,或者在识别成功回调触发时才异步加载并按需实例化,至少给个加载提示,否则体验很差。
1.4 为什么要认真对待基础准备工作
这三件事看起来都是“配置”类工作,但处理不好,后面排查问题时你会反复怀疑代码写错了,实际上从源头就是版本组合和资源策略出了问题。跨平台 AR 项目的本质是环境工程,不是纯粹的业务开发。把最底层的 SDK 关系理清楚,后面 Android 和 iOS 的问题会少一大半。
2. Android 端的发布链路:环境、清单、权限一个都跑不掉
Android 发布遇到的坑,比 iOS 更“配置向”,主要集中在构建环境、Manifest 合并和运行时权限三个层面。每一个都有典型的报错特征和排查路径。
2.1 Gradle 构建失败:先看 wrapper 再看 AGP 版本
用 Unity 直接 Build Android 时,最常遇见的报错是Gradle task assembleRelease failed with exit code 1。这个报错信息特别模糊,真正的原因要看日志里更靠前的部分。我在项目中排查时发现,绝大多数情况是 Gradle 版本、Android Gradle Plugin(AGP)版本和编译 SDK 版本不匹配。
我的排查顺序很固定:先在工程的gradle-wrapper.properties里看distributionUrl指向的 Gradle 版本,再去看自定义 Gradle 模板里声明的 AGP 版本。Unity 自带的构建脚本对两者有隐藏的兼容范围,一旦用了过新或过旧的版本,就会出现类似Minimum supported Gradle version is 6.7.1的提示。遇到这种,直接回退到官方模板对应的默认版本就好,别自己硬改。
另外,如果你的项目接入了第三方 Android 原生插件,还要确认这些 AAR 要求的 minSdkVersion 和 targetSdkVersion 不能高于工程设置的版本,否则编译时会报Manifest merger failed。
2.2 Manifest 合并冲突:provider 和命名空间是重灾区
当工程里同时存在 easyAR 和录屏、分享、统计之类的插件时,AndroidManifest 合并冲突几乎是必然的。典型报错是:
Manifest merger failed : Attribute application@name value=(... ) from AndroidManifest.xml或者Suggestion: add 'tools:replace="android:name"'。
原因通常很简单:多个插件都在清单里声明了同一个android:name的 provider 或 service。解决办法是在主工程Assets/Plugins/Android/AndroidManifest.xml的根节点加上xmlns:tools,然后在对应冲突属性上用tools:replace指定覆盖。
-keep class `你的easyAR包名`.** { *; } -keepclassmembers class `你的easyAR包名`.** { native <methods>; }这种 keep 规则要放在项目自己的 proguard 文件里,并在 Player Settings 中开启 Minify 时引用。实测下来,不开混淆的版本在 Debug 包上没问题,但 Release 包一混淆就报UnsatisfiedLinkError,所以规则一定要验证到位。
2.3 运行时授权白屏:把初始化放到权限回调之后
Android 6.0 以后,相机和麦克风都是运行时权限。AR 应用首启时会弹出系统权限对话框,但 Unity 场景的Start()方法在权限弹窗出现之前就已经执行了。如果代码在Start()里直接初始化 easyAR 并打开相机,第一帧就是黑屏或白屏,用户要切后台再回来才能恢复预览。
我之前在这个问题上卡了很久,后来换成了授权遮罩方案:进入 AR 场景前先检测权限,未授权时弹一个半透明页面解释用途,等用户授权回调成功后再创建 AR 场景并初始化 SDK。这样既解决了首帧白屏,也避免了用户习惯性拒绝后 App 毫无反应的问题。
2.4 生命周期回调:退后台、回前台、相机被占用的连锁反应
Android 端还有一个隐蔽问题:应用退后台再恢复时,相机预览偶尔会卡死或花屏。这多半是因为没有正确处理 easyAR 的生命周期,SDK 底层引擎在后台被系统挂起,回到前台后没有恢复渲染。
我的做法是在 MonoBehaviour 的OnApplicationPause和OnApplicationFocus里做对应处理:暂停时让 AR 引擎暂停渲染并释放相机预览,继续时重新恢复。这里要注意,OnApplicationPause(true)和OnApplicationFocus(false)在 Android 上会先后触发,不要重复调用同一接口导致状态错乱。
3. iOS 端的发布链路:权限描述、ATS 与构建产物
iOS 端的问题风格和 Android 完全不同。Android 大多是构建和合并配置,iOS 则主要集中在权限描述、网络策略和 Xcode 构建产物上。一个共同点是:报错信息都不够直观,需要靠日志和日常经验预判。
3.1 证书、描述文件与测试机轮换
从 Unity 构建出 Xcode 工程后,第一关是签名。新手最常遇到的报错是No profiles for ... were found,或者Provisioning profile does not support ...。这不是 Unity 的问题,而是描述文件里的设备 UDID 没有包含当前测试的 iPhone。
我的流程是:先在开发者后台把测试机 UDID 加进描述文件,下载更新后的描述文件装到本机,再让 Xcode 自动签名。这个步骤做对了,Unity 侧不需要额外设置,构建产物直接打开就能跑。需要强调一点:签名这块保持简单,别手动指定太具体的 profile,交给自动签名更省心。
3.2 权限描述缺失:一碰就闪退,日志还没有明显错误
iOS 对权限描述是“硬约束”:如果Info.plist里没有对应用途描述,调用相机、麦克风或保存到相册时 App 会直接闪退,而且在 Xcode 控制台里只有非常笼统的异常信息。这个问题的可怕之处在于它不报错名,新手很难联想到是权限描述缺失。
我的做法是提前把几个描述一次补齐。在Info.plist里增加:
<key>NSCameraUsageDescription</key> <string>需要使用相机进行AR识别</string> <key>NSMicrophoneUsageDescription</key> <string>需要在录制视频时采集声音</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>需要保存录制的视频到相册</string>即使当前版本只用了相机,麦克风和相册描述也建议先写上去,因为录屏功能很可能会用到其中一项,等你人已经在用户手里了才发现描述缺失,补丁和审核周期会很难受。
3.3 ATS 限制:HTTP 资源被拦截
easyAR 项目里经常要加载远程识别图、模型或云端配置。如果这些资源地址没有使用 HTTPS,iOS 会直接拦截,控制台报错写着App Transport Security has blocked a cleartext HTTP。
测试阶段可以用例外配置,在Info.plist里加:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>但我不建议正式包全局放开 ATS。更稳妥的做法是给特定的测试域名加例外,生产环境全部改用 HTTPS 资源。这个习惯在 Android 侧也要注意,Android 9 以后默认禁止明文 HTTP,需要单独配置networkSecurityConfig。
3.4 Framework、Bitcode 与系统版本策略
旧的 Unity 工程在构建 iOS 时会默认开启 Bitcode,而现在的新 Xcode 对 Bitcode 支持已经逐步收紧,构建很容易报警告甚至错误。到 SDK 相关插件的 Framework 不一定带 Bitcode 符号,所以建议直接在 Unity 构建导出的 Xcode 工程里把Enable Bitcode设为 NO。
渲染方面,easyAR 在 Metal 环境下表现更稳,旧设备偶尔出现MTLTexture cannot be used这类问题,多半和 iOS 版本太低有关。我的做法是把最低系统版本设置在能稳定支持 Metal 的版本以上,并避免使用 OpenGLES 作为备用渲染后端。真机上预览和录屏的表现都更正常。
4. easyAR recording 录屏接入:从能用到一个一个坑
录屏是这次项目里耗时最长、问题最“丰富”的模块。easyAR SDK 提供的录屏功能和系统录屏不一样,它是面向 AR 相机画面的视频编码能力,你可以设置分辨率、码率、帧率、是否采集麦克风,并把相机画面和渲染内容合成为视频文件。理解这一点是避坑的基础。
4.1 基础用法:把“相机画面”而不是“屏幕”给录制器
录屏组件的核心逻辑类似这样:初始化录制器,设置输出参数,把 AR 相机渲染到的 RenderTexture 持续传给录制器,开始和结束录制,最后拿到文件路径。
我一开始误以为录屏会自动包含所有 UI 层,结果录出来的视频里只有 AR 模型和相机画面,界面上的按钮和提示一个都没有。后来才意识到,渲染到屏幕上的东西和送进编码器的纹理是两条管线。如果你需要 UI 也出现在视频里,必须把 UI 单独渲染到同一张 RenderTexture,或者用额外的叠加层。这个容易忽略导致返工。
4.2 黑屏、绿屏和花屏:纹理格式与分辨率是关键
录屏最常见的问题是输出文件黑屏。我排查后发现,原因是录制纹理格式和编码器要求不匹配,或者没有在正确的渲染回调时机把纹理填进去。建议固定使用 RGBA 格式的 RenderTexture,并在渲染管线里确认纹理指针在每一帧都被正确更新。
还有一个容易踩的坑是宽高必须是偶数。Android 的硬件编码器对奇数分辨率支持不稳定,1281x721 这种尺寸就会绿屏或花屏,改成 1280x720 就正常了。如果你的识别区域比例比较特殊,先做裁剪或加黑边,保证送入编码器的尺寸合法。
4.3 录完没有声音:麦克风权限和采样率设置
录屏没有声音时,第一反应不应该是去找音频 API,而是先查权限。Android 端要在开始录制之前动态申请RECORD_AUDIO,等授权成功后再启动录制,否则编码器会静默地只录画面。
iOS 端则要确认NSMicrophoneUsageDescription已配置,并且用户首次授权弹窗没有被拒绝。如果用户之前误拒了,不会自动再弹,需要在界面里引导跳系统设置重新开启权限。
音画不同步问题也不少见。编码器的音频采样率如果和采集端不一致,时间越长偏移越明显。固定采样率,比如统一用 48kHz,同时录制过程中不要频繁切换音频设备,基本能解决。
4.4 竖屏录出横屏:旋转元数据不可靠
竖屏的 AR 应用录出来的视频,在部分播放器里会横过来或方向错乱。视频文件的旋转信息是通过元数据保存的,但 Android 很多播放器并不尊重这个元数据。
最可靠的办法不是在事后旋转视频,而是在录制时就把方向设置正确。如果 SDK 的录制器提供了方向参数,就显式传入竖屏方向;如果没有,建议把 RenderTexture 渲染阶段就按目标方向处理,避免依赖播放器行为。iOS 端相对好一些,但也要检查AVAsset的输出方向是否符合预期。
4.5 录制中断导致文件损坏:生命周期要管住
录制过程中用户切后台、接电话、锁屏,录制器如果继续写文件,产出的视频经常是损坏的,无法播放。我的处理方式是在OnApplicationPause(true)回调里立即停止录制并关闭输出,同时记录一个“录制被中断”的状态,回前台后提示用户并允许重新录制。
停止录制的时机也要注意:调用停止接口后,文件可能还在收尾写入,不要马上读取文件大小或路径去分享,等录制器的状态回调确认已经进入停止完成状态后再操作。
4.6 Android 与 iOS 保存相册的两条链路
录制完的视频默认存在应用私有目录,要保存到系统相册,Android 和 iOS 做法完全不同。Android 上可以用MediaStore插入一条视频记录,老版本设备则用MediaScannerConnection.scanFile触发相册扫描。iOS 上要先申请相册权限,再用PHPhotoLibrary执行保存,注意保存完成回调是异步的,需要更新界面时不要在主线程阻塞。
5. 识别率与跟踪稳定性:从本地图到真实世界的光线战争
录屏搞定后,识别率又成了另一个战场。easyAR 的图片识别本质是特征点匹配,很多“识别的效果差”的反馈,到最后都发现是识别图选择不当或环境因素干扰,而不是代码 Bug。
5.1 识别图选图:对比度、非对称、避免大面积重复
选识别图的艺术在于让算法有足够多的、稳定的特征点可抓。纯色背景、大面积模糊区域、上下完全对称的图案,识别率都很低。最优的是高对比、纹理丰富、非对称的图,比如杂志封面、带文字的墙贴、有明显几何结构的产品包装。
打印机打出来的实体图测试时,要注意纸张反光。真机的自动白平衡和强光反射会让特征点的亮度和颜色发生变化,导致在编辑器里明明识别很快,真机上却半天没反应。给实体识别图加哑光覆膜比换算法参数更有效。
5.2 本地加载与远程下发:数据库重建不能频繁触发
识别图的加载有两种方式:本地随包打包和远程下发。本地加载简单稳定,但换图必须发版;远程下发灵活,但要处理网络失败、缓存、版本同步。我在项目里采用本地基础图集加远程增量图集的方式,启动时先加载本地,再在后台异步拉取云端列表并逐个加入数据库。
这里有一个很隐蔽的坑:频繁向数据库添加或移除识别图会导致短暂的追踪卡顿,因为算法在重建索引。正确做法是批量操作,并且不要在 AR 场景进行过程中持续增删,尽量在启动阶段或切场景阶段一次性完成。
5.3 多目标同时跟踪:性能预算要提前做
同时识别多张图时的性能开销不是简单线性增加。设备不同,可用的同时跟踪数量差异很大。我在中低端 Android 机上测试过,同时跟踪 4 张图以上时帧率下降明显。
如果要保证流畅度,我的做法是限制同时跟踪数量,对距离最近的 1 到 2 个目标做高精度渲染,其余目标只维持识别状态不实例化复杂模型。UI 层也可以通过目标列表选择的方式引导用户聚焦到某一个 AR 内容。
5.4 跟踪丢失与恢复:平滑过渡避免画面跳变
相机大幅度晃动、遮挡目标超过半秒、光线剧烈变化,跟踪都会丢失。代码上要监听跟踪状态从“正在跟踪”变成“丢失”,再变回“重新跟踪”的整个过程。如果每次重新识别都重新刷新模型位置,AR 物体会明显跳动或者角度突变。
我用缓存位置和朝向的方式做平滑过渡,在跟踪丢失期间隐藏物体或淡出,重新命中后从缓存姿态附近逐步靠拢,体验自然很多。
6. 我的排查套路与顺手工具,给后来者抄作业
到这里,整个项目的核心问题基本都踩完了。最后一节想聊聊排错方法论,因为我发现很多人卡住不是因为问题复杂,而是排查方向太散,一会在渲染上找原因,一会去查算法参数,最后什么都没解决。
6.1 三路日志一起看:Unity 控制台之外的信息
Unity 控制台只显示 C# 层的输出,很多底层错误需要看系统日志。Android 上用 adb logcat,过滤关键字比如EasyAR、Unity、Camera、Recorder,按时间戳对齐到 Unity 日志的帧号,能快速判断问题是出在渲染、生命周期还是权限。
iOS 上直接连 Xcode 控制台,同样过滤关键词,注意设备日志和控制台日志要区分开。多插件的工程里,日志中最容易误导人的是第三方 SDK 的干扰性报错,先确认报错来自哪个模块,再去工程里定位。
6.2 最小复现工程:把变量降到最少
遇到诡异问题时,我习惯立刻新建一个空场景,只保留相机、一个识别图、一个录制器,其他插件全部移除,先看问题是否复现。能复现,说明问题出在 easyAR 本身或我的用法上;不能复现,就说明第三者插件污染了上下文,比如提前改掉了 OpenGL 状态或抢占了麦克风。
这个方法救过我很多次,尤其适合录屏黑屏这类现象型问题。别指望靠读代码就能想明白渲染层面的干扰,跑一个干净场景出的结论比任何推理都可靠。
6.3 打包体检表:每次记录版本组合和测试机型
跨平台问题里,最容易让人觉得“莫名其妙”的是版本组合。我后来养成了一个习惯:每次打包都在表格里记录 Unity 版本、easyAR 版本、Gradle 模板是否自定义、目标平台、测试机型、系统版本。问题出现时,回退某一项组件,基本能锁定元凶。
Unity: 2021.3.20f1 easyAR: 4.x Android SDK: 33 Gradle: 6.7.1 机型: 某中端Android / iPhone 12这个表格看着简单,实际排查中能节省大量时间。
6.4 真机永远比编辑器更接近真相
编辑器里的识别率、帧率、录屏效果仅供功能测试参考,真机上的相机白平衡、自动对焦、陀螺仪数据都会影响 AR 表现。我建议拿到测试机后先做三件事:基础识别测试、光线变化测试、录屏完整流程测试。这三条过了,再谈后续优化;每一条不过,按上面对应的章节去查,链路清晰很多。
最后再分享一个小技巧:我在工程里内置了一个自检测面板,把 easyAR 初始化、权限申请、识别图加载、录屏开始和停止分别用日志点标记,线上用户反馈问题时,靠上报的日志点能快速判断卡在哪一步。跨平台 AR 发布,本质上是环境差异的博弈,把每个环节的可观测性做到位,再乱的坑也能一个个填平。