掉帧卡顿排查清单
现象分类
AR 应用的性能问题表现各异,不同根因:
| 现象 | 可能根因 | 排查方向 |
|---|---|---|
| 全局 FPS 低于 30 | 整体渲染管线太重 | Draw Call、分辨率、材质 |
| 偶尔 FPS 掉到个位数 | 主线程有阻塞操作 | GC、IO、后台任务 |
| AR 物体抖动 | SLAM 跟踪质量差 | 环境特征点、光照 |
| 只有遮挡开启时掉帧 | 遮挡渲染管线开销 | 深度缓冲、Shader 复杂度 |
| 游戏场景大量物体时掉帧 | 单帧 Draw Call 过多 | Instancing、批处理 |
| 特定设备才掉帧 | 设备性能差异 | GPU 型号、是否支持 Vulkan |
排查流程
掉帧现象出现
↓
第一步:用 Android Profiler / Unity Profiler 定位瓶颈阶段
↓ 瓶颈在 CPU ↓ 瓶颈在 GPU ↓ 瓶颈在 IO
第二步:分析 CPU 耗时 第三步:分析 GPU 耗时 第四步:检查后台阻塞
↓ ↓ ↓
定位到具体函数 定位到 Pass 定位到具体 IO 操作
↓ ↓ ↓
针对性优化 针对性优化 移到后台线程
第一步:定位瓶颈阶段
Android Profiler
Android Studio → Profiler → 选择你的进程 → 查看 Call Chart(CPU) 和 Memory:
- CPU 曲线是否一直高(> 100ms/帧)?
- 是否有周期性尖峰(GC 或某个周期性任务)?
- 哪个函数占了最多时间?
Unity Profiler
Window → Analysis → Profiler:
- CPU Usage → 看 Camera.Render、你的脚本、Physics 哪个最重
- Rendering → 看 SetPass Calls / Draw Calls / Batches 数量
- Memory → GC Allocation 是否每帧 > 50KB
快速判断:CPU-bound vs GPU-bound
| 判断 | CPU-bound | GPU-bound |
|---|---|---|
| 开启/关闭遮挡渲染 | 不影响 FPS | 影响明显 |
| 降低渲染分辨率到 50% | 影响不大 | FPS 大幅提升 |
| 减少 Draw Call | 影响不大 | FPS 提升 |
| 复杂 CPU 逻辑(物理、AI) | 关闭后 FPS 提升 | 不影响 |
第二步:CPU 侧优化
最常见的瓶颈来源
| 来源 | 特征 | 优化手段 |
|---|---|---|
| Session.update() 后重计算 | update() 之后你的代码做了重逻辑 | 移到后台线程或协程 |
| GC 频繁 | FPS 周期性掉帧(每几秒一次) | 减少每帧 new 对象,用对象池 |
| 每帧遍历大量 Trackable | 有几百个 Trackable 时 slow | 只在需要时遍历,缓存引用 |
| 物理引擎(如 Unity Physics) | 复杂碰撞场景 | 降低碰撞检测精度,用 Layer 过滤 |
| Cloud Anchors 托管/解析 | Host Resolve 时卡一下 | 完全后台,不阻塞渲染线程 |
GC 专项治理
// 错误:每帧 new 对象
fun onDrawFrame() {
val list = mutableListOf<Anchor>() // 每帧创建新 List → GC 频繁
session.getAllAnchors(list) // 其实可以传入复用的 List
}
// 正确:复用对象
private val anchorList = mutableListOf<Anchor>() // 类成员变量
fun onDrawFrame() {
anchorList.clear()
session.getAllAnchors(anchorList) // 复用同一个 List
}
Unity 中的 GC Alloc 治理
// 每帧不能分配新内存!
void Update()
{
// 错误
var hits = new List<ARRaycastHit>(); // 每帧 new → GC Alloc
raycastManager.Raycast(Input.GetTouch(0).position, hits);
// 正确:在类定义中分配一次
// private List<ARRaycastHit> hits = new List<ARRaycastHit>();
hits.Clear();
raycastManager.Raycast(Input.GetTouch(0).position, hits);
}
Profiler 中 GC Allocation 在绿色区域每帧都有意味着泄漏型分配——必须全部清掉。
第三步:GPU 侧优化
Draw Call 过多
优化手段:
1. Static Batching:勾选静态物体,Unity 自动合并(GPU 优化)
2. GPU Instancing:同类物体用 Instanced Rendering
3. Texture Atlas:多张小贴图合并成大图集,减少 Material 切换
4. 减少透明物体(透明物体不能被批处理)
材质 Shader 过重
检查清单:
- Shader 有几个 Pass?每个 Pass 都是一次额外渲染
- 开了 Shadow 吗?AR 场景中阴影开销巨大
- 开了实时反射吗?Reflection Probe 开销大
- 用的 Shader 是移动端兼容的吗?
- 有没有在 Shader 中做复杂计算(如 Fresnel、SSAO)?
Unity URP 渲染路径优化
// URP Asset 设置
URPAsset.renderScale = 0.75f; // 降低分辨率
URPAsset.msaaSampleCount = 0; // 关闭 MSAA(AR 不需要)
URPAsset.supportsCameraDepthTexture = true; // 深度贴图开启(遮挡需要)
第四步:特殊场景优化
遮挡渲染优化
// 低端设备降级策略
if (IsLowEndDevice())
{
// 关闭遮挡(省了整个深度缓冲写入 + 遮挡 Pass)
occlusionManager.environmentOcclusionMode = EnvironmentOcclusionMode.Disabled;
}
else
{
// 简化遮挡(只用 Depth 模式,不用 Human Stencil)
occlusionManager.environmentOcclusionMode = EnvironmentOcclusionMode.Automatic;
}
Depth API 处理优化
// 不要在每帧用 acquireDepthImage() 然后在 CPU 端遍历
// 正确做法:
// - 渲染管线直接用深度纹理(GPU 处理)
// - 只有特殊计算(如尺寸测量)才在 CPU 端读深度值
// - 读深度值时只读必要区域,不要遍历整张图
多人 AR 场景优化
Cloud Anchors 场景的额外开销:
1. 接收/发送虚拟物体位置数据(如果有实时同步)
→ 用频率限制(比如每 100ms 同步一次,而不是每帧)
2. 同时渲染多个用户的虚拟物体
→ 远处物体用 LOD(Level of Detail)简化
→ 完全不渲染离相机太远的物体(Frustum Culling)
3. Cloud Anchors Resolve 时的回调
→ Resolve 回调不在渲染线程执行,不要做重操作
设备特定问题
问题:某款手机上 FPS 特别低
| 手机型号 | 可能原因 | 解决 |
|---|---|---|
| 三星 S22/S23 系列 | 默认开启 Vulkan,但 ARCore 不支持 | Unity Build Settings → Graphics APIs → 移除 Vulkan |
| 小米/红米系列 | MIUI 可能限制后台进程 | 不在后台创建新进程就行 |
| 华为/荣耀 | AREngine 可能抢占摄像头 | 确保使用 ARCore 而不是 AREngine |
| iPhone(老机型) | 硬件性能不足 | 自动降低渲染分辨率 |
| Google Pixel | 通常是最优适配 | 参考 Pixel 系列做兼容性测试 |
自动检测设备能力分级
enum class DeviceTier { HIGH_END, MID_RANGE, LOW_END }
fun getDeviceTier(context: Context): DeviceTier {
// GPU 性能
val gl = EGLContext.getCurrent().gl as GLES20
val gpuMemory = getGpuMemorySize() // 需要实现
// 屏幕分辨率
val displayMetrics = context.resources.displayMetrics
val resolution = displayMetrics.widthPixels * displayMetrics.heightPixels
return when {
gpuMemory >= 800 && resolution >= 2000000 -> DeviceTier.HIGH_END
gpuMemory >= 400 && resolution >= 1000000 -> DeviceTier.MID_RANGE
else -> DeviceTier.LOW_END
}
}
然后根据设备 Tier 自动调整:
- 渲染分辨率(HIGH: 100%, MID: 75%, LOW: 60%)
- 遮挡渲染级别
- Draw Call 数量预算
- 帧率上限