性能监控基础:常见性能指标(帧率、延迟、内存、CPU)

做 CameraX 启动优化,其实就是搞定四个数字:帧率、延迟、内存、CPU。我个人习惯把这四个指标称为「性能四件套」。少一个,你就算优化了也不知道优化到哪去了。我曾经吃过大亏——只盯着帧率,结果内存蹭蹭涨,最后被系统直接杀了进程。

嗯,那咱们就一个一个捋清楚。每个指标我会告诉你它是什么、怎么测、以及我在项目中踩过的坑。

CameraX 启动性能监控指标体系 CameraX 启动性能 帧率 (FPS) 延迟 (Latency) 内存 (Memory) CPU 使用率 相互影响 优化时这四个指标必须一起看,不能只盯一个

帧率:画面流畅度的直接体现

帧率说白了就是每秒钟画面刷新的次数。CameraX 启动时,如果帧率低于 20fps,用户体验就崩了。我习惯用 Choreographer.FrameCallback 来监控,简单实用。

// 个人常用的帧率监控工具
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTimeNs = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNs != 0) {
            float fps = 1_000_000_000f / (frameTimeNanos - lastFrameTimeNs);
            Log.d("FpsMonitor", "当前帧率: " + fps);
        }
        lastFrameTimeNs = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

为什么 CameraX 启动时特别容易出现掉帧?因为启动瞬间要加载预览流、开启相机设备、分配 Surface 等等。我记得有一次项目里,启动帧率从 30fps 掉到 12fps,查了半天发现是 CameraXConfig 设置里有个 setCameraExecutor 没处理好线程优先级。

小技巧: 用 Trace.beginSection("startup") 结合 Systrace 看帧率掉在哪个阶段,比单纯 Log 精准很多。

延迟:用户感知最大的痛点

延迟包括启动延迟、预览延迟和拍照延迟。对于 CameraX 启动来说,我关注的是「从 ProcessCameraProvider.getInstance 到 PreviewView 显示第一帧」的总耗时。你想想看,用户打开相机,等了 2 秒还是一片黑,他肯定觉得 App 卡死了。

统计延迟我喜欢用 reportFullyDrawn() 配合 ActivityLifecycleCallbacks。具体做法是在 onCameraReady 回调里打点。

// 启动延迟上报示例
fun reportStartupLatency() {
    val startTime = System.currentTimeMillis()
    val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
    cameraProviderFuture.addListener({
        val latency = System.currentTimeMillis() - startTime
        Log.i("LatencyMonitor", "CameraX 启动延迟: $latency ms")
    }, ContextCompat.getMainExecutor(context))
}

我曾经在一个低端机上测到 1800ms 的延迟,原因竟是 ImageCapture 的 captureMode 配置为 CAPTURE_MODE_MAXIMIZE_QUALITY,导致 CameraX 内部做了大量图像处理。改成 CAPTURE_MODE_MINIMIZE_LATENCY 后直接降到 800ms。

避坑指南: 延迟监控一定要区分「首次启动」和「后续启动」。首次启动有 CameraProvider 初始化、权限检查等,延迟会偏高。我建议单独上报这两个场景。

内存:别让 CameraX 变成内存杀手

CameraX 启动时内存会快速上升,主要是 Surface 缓冲区和图像流占用的。我们一般盯着两个数据:Java Heap 和 Native Heap。Android Studio 的 Memory Profiler 可以看,但上线后我推荐用 Debug.MemoryInfo 打点。

内存类型监控方式常见问题
Java HeapRuntime.getRuntime().totalMemory()Bitmap 没及时回收
Native HeapDebug.getNativeHeapSize()Surface 缓冲区泄漏
Graphic 内存ActivityManager.getProcessMemoryInfo()预览帧堆积

我遇到过最诡异的 Case:CameraX 退到后台后,内存一直居高不下。查了两天发现是 Preview.SurfaceProvider 没有在 onStop() 里清空。嗯,这个坑其实文档里写了,但没写清楚——如果你用了自定义 SurfaceProvider,一定要在生命周期结束调用 surface.getHolder().removeCallback(this)。

CPU 使用率:负载平衡的艺术

CPU 占用高不一定是坏事,但 CameraX 启动时如果 CPU 长时间跑满,说明有瓶颈。我建议同时看两个维度:

  • 总 CPU 使用率:用 /proc/stat 读取整体负载
  • 进程 CPU 使用率:用 /proc/[pid]/stat 算自己的占比
// 简易的进程 CPU 监控(生产环境建议用 Profilo 或 atrace)
fun getProcessCpuUsage(): Float {
    val pid = Process.myPid()
    val stat = File("/proc/$pid/stat").readText().split(" ")
    // utime + stime 是 CPU 时间片
    val utime = stat[13].toLong()
    val stime = stat[14].toLong()
    val totalTime = utime + stime
    // 需要与系统总时间做比例计算,这里仅示例
    return totalTime / 100f // 简化示意
}

为什么 CameraX 启动会让 CPU 飙升?因为 CameraX 内部使用了多个线程池(CameraExecutor、IoExecutor、MainExecutor),如果它们竞争厉害,CPU 就炸了。我建议把 setCameraExecutor 绑到 AsyncTask.THREAD_POOL_EXECUTOR 或者自定义固定大小线程池上,避免无限扩张。

核心观点: 帧率、延迟、内存、CPU 这四个指标像四个轮胎。一个轮胎瘪了,车就跑不稳。我在项目里会把这四个数据打到同一个埋点日志里,方便关联分析。举个例子:发现帧率低的同时 CPU 使用率也低,那可能是锁等待;如果 CPU 高但帧率低,那大概率是渲染管线阻塞。

指标间的关系矩阵

把它们的关系列出来,你就能一眼看懂怎么排查了:

  • 帧率低 + 延迟高 → 一般是主线程被阻塞了,检查 SurfaceView 的 onFrameAvailable
  • 内存高 + 帧率低 → 可能 GC 频繁,检查 ImageProxy 是否及时关闭
  • CPU 满载 + 延迟正常 → 某个后台任务在抢线程,检查 ImageAnalysis 的分辨率设置
  • 四个指标都正常,但用户觉得卡 → 嗯,这可能是动画帧率问题,不是 CameraX 本身的问题

我在一次线上巡检中发现 2% 的用户启动延迟超过 3s,但内存和 CPU 都正常。后来定位到是他们手机温度过高触发了系统的 thermal throttling —— 这种场景下,我们的监控指标要加上设备温度才好分析。

个人习惯: 我通常会写一个 PerformanceMonitor 的单例,每 200ms 采样一次这四个指标,并打一个时间戳。上线后用 Timber.i 输出到日志文件,配合 Crash 分析一起看。这样能还原问题现场,比事后蒙着改代码强多了。