24、性能优化:渲染帧率监控、内存泄漏排查、CPU/GPU负载均衡、ANR预防

性能优化这件事,说实话,是音视频开发里最磨人的环节。你辛辛苦苦把功能跑通了,结果一上真机,卡顿、掉帧、闪退,用户直接给你一个差评。我这些年踩过的坑,十个手指头都数不过来。今天咱们就把这块硬骨头啃下来。

核心要点:音视频性能优化的四个维度——帧率、内存、CPU/GPU、ANR。缺一不可,环环相扣。

音视频性能优化四维体系 性能优化 核心目标 渲染帧率监控 Choreographer / SurfaceFlinger 内存泄漏排查 MAT / LeakCanary / hprof CPU/GPU负载均衡 硬件加速 / 渲染管线 ANR预防 主线程监控 / 异步化 四个维度相互影响,需要综合调优

一、渲染帧率监控——别让用户觉得卡

帧率监控,说白了就是看你的画面每秒能刷多少帧。Android 上标准是 60fps,也就是每帧 16.6ms。一旦超过这个时间,掉帧就来了。

我个人习惯用 Choreographer.FrameCallback 来做实时监控。为什么不用 Profile 工具?因为线上环境你没法挂 Profiler,得自己埋点。

// 帧率监控核心代码
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    private long lastFrameTimeNanos = 0;
    
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos == 0) {
            lastFrameTimeNanos = frameTimeNanos;
            Choreographer.getInstance().postFrameCallback(this);
            return;
        }
        
        long diffMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
        float fps = 1000f / diffMs;
        
        // 掉帧检测
        if (diffMs > 16.6f) {
            int droppedFrames = (int)(diffMs / 16.6f);
            Log.w("FpsMonitor", "掉帧 " + droppedFrames + " 帧, 当前FPS: " + fps);
            // 上报到监控平台
        }
        
        lastFrameTimeNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

我的经验:曾经有个项目,视频播放器在低端机上卡成PPT。一查发现是每帧都做了 Bitmap 解码。后来改成复用解码结果,帧率直接从 15fps 飙到 55fps。记住:不要在 UI 线程做任何 IO 操作

除了 Choreographer,你还可以用 FrameMetricsAggregator 来统计帧耗时分布。这个 API 在 Android 7.0 以上可用,能帮你精确看到每一帧的绘制、布局、输入处理时间。

二、内存泄漏排查——看不见的杀手

内存泄漏在音视频应用里特别常见。为什么?因为视频帧、音频缓冲区、Surface 对象,这些家伙生命周期长,一不小心就泄漏了。

我排查内存泄漏的套路是这样的:

  1. 先用 LeakCanary 自动检测——开发阶段必开,它能告诉你哪个 Activity 没释放
  2. 再用 Android Profiler 抓 hprof——线上问题用 MAT 分析
  3. 最后看 Dominator Tree——找最大的对象,看 GC Root 链

举个例子,视频播放器里最常见的泄漏:

// 错误写法——匿名内部类持有Activity引用
MediaPlayer player = new MediaPlayer();
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        // 这里隐式持有外部Activity引用
        showResult(); 
    }
});

// 正确写法——使用静态内部类 + 弱引用
private static class MyCompletionListener implements MediaPlayer.OnCompletionListener {
    private WeakReference<VideoActivity> activityRef;
    
    MyCompletionListener(VideoActivity activity) {
        activityRef = new WeakReference<>(activity);
    }
    
    @Override
    public void onCompletion(MediaPlayer mp) {
        VideoActivity activity = activityRef.get();
        if (activity != null) {
            activity.showResult();
        }
    }
}

避坑指南:我曾经遇到过一个诡异的内存泄漏——SurfaceView 在页面销毁后,底层 Surface 还在持续渲染。原因是 SurfaceHolder.Callback 没有在 onPause 时移除。记住:所有注册的监听器、回调、Handler,必须在 onDestroy 或 onPause 中反注册

三、CPU/GPU负载均衡——别让一个累死一个闲死

音视频处理是 CPU 和 GPU 协同工作的典型场景。CPU 负责解码、逻辑计算,GPU 负责渲染、特效。如果负载不均衡,要么 CPU 满载导致解码跟不上,要么 GPU 过载导致掉帧。

我建议从这几个角度入手:

  • 硬件加速开关——Android 默认开启硬件加速,但有些自定义 View 需要手动关闭
  • 渲染管线优化——减少 Overdraw,使用 clipRect 裁剪不可见区域
  • 解码器选择——MediaCodec 优先使用硬件解码,软解留给低端机

来看一个实际案例:视频编辑中的滤镜渲染。

// 使用 RenderScript 进行 GPU 加速滤镜
RenderScript rs = RenderScript.create(context);
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

// 分配内存
Allocation inputAlloc = Allocation.createFromBitmap(rs, inputBitmap);
Allocation outputAlloc = Allocation.createTyped(rs, inputAlloc.getType());

// 设置模糊半径(CPU计算参数,GPU执行渲染)
blurScript.setRadius(25f);
blurScript.setInput(inputAlloc);
blurScript.forEach(outputAlloc);

// 结果回读
outputAlloc.copyTo(outputBitmap);

// 记得释放
inputAlloc.destroy();
outputAlloc.destroy();
blurScript.destroy();
rs.destroy();

关键点:CPU 负责「算」——比如计算滤镜参数、坐标变换;GPU 负责「画」——比如像素着色、混合。把计算密集的任务交给 CPU,把渲染密集的任务交给 GPU,这才是合理的分工。

你想想看,如果让 CPU 去逐像素处理 1080p 的视频帧,那画面太美我不敢看。反过来,如果让 GPU 去解码 H.264,它也没那个能力。各司其职才是王道。

四、ANR预防——别让系统把你杀了

ANR(Application Not Responding)是 Android 系统对用户的「保护机制」。说白了,就是你的主线程被堵死了,系统帮你强制退出。

音视频应用里 ANR 的高发场景:

场景 原因 解决方案
视频解码在主线程 MediaCodec 同步调用阻塞 使用异步模式或子线程解码
文件 IO 在主线程 读取大文件耗时 使用协程或线程池
网络请求同步等待 网络超时导致阻塞 全部异步化
Bitmap 解码 大图解码耗时 使用 BitmapFactory.Options 降采样

我自己的 ANR 预防三板斧:

  1. BlockCanary 监控主线程——超过 2 秒的卡顿直接报警
  2. StrictMode 严格模式——开发阶段开启,所有主线程 IO 都会 crash
  3. 异步化改造——所有耗时操作扔到子线程,用 Handler 或 LiveData 回传结果
// 使用协程处理视频解码,避免主线程阻塞
lifecycleScope.launch(Dispatchers.IO) {
    val decoder = MediaCodec.createDecoderByType("video/avc")
    decoder.configure(format, surface, null, 0)
    decoder.start()
    
    // 解码循环在 IO 线程
    while (isActive) {
        val inputIndex = decoder.dequeueInputBuffer(10000)
        if (inputIndex >= 0) {
            val inputBuffer = decoder.getInputBuffer(inputIndex)
            // 填充数据...
            decoder.queueInputBuffer(inputIndex, 0, size, pts, 0)
        }
        
        val outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000)
        if (outputIndex >= 0) {
            decoder.releaseOutputBuffer(outputIndex, true)
        }
    }
    
    // 切回主线程更新 UI
    withContext(Dispatchers.Main) {
        updateProgress()
    }
}

我的习惯:每次写完音视频相关代码,我都会用 adb shell dumpsys cpuinfoadb shell dumpsys gfxinfo 看一眼资源占用。如果发现主线程 CPU 占用超过 10%,那就要警惕了。

嗯,这里还要注意一点:ANR 的阈值在不同 Android 版本上不一样。Android 10 之前是 5 秒,之后是 2 秒。所以你的监控阈值也要跟着调整。

五、综合调优思路

这四个维度不是孤立的。内存泄漏会导致 GC 频繁,GC 会卡住主线程,主线程卡顿就掉帧,掉帧严重就 ANR。你看,一条链全串起来了。

我个人的调优顺序是:

  • 先修内存泄漏——这是根本,内存稳了 GC 就少
  • 再优化帧率——用 Choreographer 监控,找到掉帧点
  • 然后调 CPU/GPU 负载——用 Profile 工具看哪个核心在偷懒
  • 最后防 ANR——把前面三步做好,ANR 自然就少了

说白了,性能优化没有银弹。你得拿着工具一点点测,一点点改。我见过太多人一上来就上大招,结果越优化越卡。记住:先测量,再优化,再测量

总结一句话:帧率监控是眼睛,内存排查是体检,CPU/GPU 均衡是健身,ANR 预防是保险。四者缺一不可,才能做出流畅的音视频应用。


公众号:蓝海资料掘金营,微信deep3321