28、性能优化与内存管理:内存泄漏检测、CPU/GPU占用优化、电量优化

各位同学,欢迎来到第28讲。到了这个阶段,咱们的RTMP推流器基本功能已经跑通了。但说实话,能跑和跑得好,中间差着十万八千里。我见过太多项目,功能做完了,一上真机就发热、卡顿、闪退。今天这一讲,我们就来聊聊怎么把这些“隐形炸弹”一个个拆掉。

28.1 内存泄漏检测:别让内存悄悄溜走

内存泄漏,说白了就是对象用完了,但GC回收不掉。在音视频场景里,这个问题尤其致命。你想想看,推流过程中不断创建新的帧数据、编码器实例、网络缓冲区,如果这些对象泄漏了,内存占用会像滚雪球一样越滚越大,最后直接OOM。

核心原则:谁创建,谁释放。生命周期长的对象,不要持有生命周期短的对象的引用。

28.1.1 常见泄漏场景与检测工具

我在项目中遇到过最典型的泄漏,就是MediaCodec回调泄漏。很多同学在Activity里直接实现MediaCodec.Callback接口,然后把这个匿名内部类传给了MediaCodec。结果Activity销毁了,MediaCodec还在工作,回调里还持有Activity的引用——泄漏就这么发生了。

检测工具我推荐三个:

  • LeakCanary:自动化检测,适合日常开发。集成简单,出问题直接弹通知。
  • Android Profiler:手动分析,适合定位具体泄漏点。我习惯用它看堆转储。
  • MAT (Memory Analyzer Tool):深度分析,适合排查疑难杂症。比如GC Root链太长的情况。

我的习惯:每次提交代码前,先跑一遍LeakCanary。如果发现泄漏,用Android Profiler抓堆转储,再用MAT分析引用链。三步走,基本能解决90%的问题。

28.1.2 音视频场景的泄漏修复

来看一个实际例子。下面这段代码,是典型的MediaCodec泄漏:

// 错误写法:匿名内部类持有Activity引用
public class PushActivity extends AppCompatActivity {
    private MediaCodec mEncoder;

    private void initEncoder() {
        mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
        mEncoder.setCallback(new MediaCodec.Callback() {
            @Override
            public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
                // 这里隐式持有PushActivity.this
                updateUI(info); 
            }
        });
    }
}

修复方案很简单:用静态内部类 + 弱引用,或者干脆在onDestroy里释放MediaCodec。

// 正确写法:静态内部类 + 弱引用
private static class EncoderCallback extends MediaCodec.Callback {
    private WeakReference<PushActivity> activityRef;

    EncoderCallback(PushActivity activity) {
        this.activityRef = new WeakReference<>(activity);
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
        PushActivity activity = activityRef.get();
        if (activity != null) {
            activity.updateUI(info);
        }
    }
}

// 别忘了在onDestroy里释放
@Override
protected void onDestroy() {
    super.onDestroy();
    if (mEncoder != null) {
        mEncoder.stop();
        mEncoder.release();
        mEncoder = null;
    }
}

警告:我曾经见过一个项目,MediaCodec的release方法没调用,结果每次推流都多占几十兆内存。推流半小时后,直接OOM。所以,所有Native资源(MediaCodec、Surface、OpenGL上下文)都必须显式释放,别指望GC帮你收。

28.2 CPU/GPU占用优化:让手机不再发烧

音视频处理是CPU和GPU的“重灾区”。编码、解码、渲染、网络传输,每一步都在消耗算力。如果优化不到位,手机分分钟变成暖手宝。

28.2.1 CPU优化:减少不必要的计算

CPU占用高的原因,无非是算法效率低或者做了无用功。我总结了几条实战经验:

  • 避免频繁创建对象:比如每帧都new一个byte[],这会让GC频繁触发。用对象池或者复用缓冲区。
  • 减少数据拷贝:从Camera到编码器,尽量走Surface输入,不要走byte[]拷贝。一次拷贝都不要。
  • 合理设置帧率:不是所有场景都需要30fps。直播教学类,15fps就够了。帧率降一半,CPU占用能降40%。
  • 使用硬件编码:MediaCodec硬件编码比软件编码(比如x264)省电得多。除非特殊需求,否则别用软编。

数据对比:我在一个项目中做过测试,同样推720p视频,硬件编码CPU占用约15%,软件编码直接飙到60%。差距就是这么明显。

28.2.2 GPU优化:减少渲染开销

GPU优化主要针对预览渲染滤镜处理。如果你用了OpenGL做美颜或特效,那GPU占用就是大头。

几个关键点:

  • 减少绘制调用:每帧的glDrawArrays调用次数越少越好。能合并的绘制操作,尽量合并。
  • 使用VBO/VAO:顶点数据不要每帧上传,用顶点缓冲对象(VBO)一次性上传。
  • 降低分辨率:预览分辨率不一定要和编码分辨率一样。预览用720p,编码用1080p,GPU占用能降一半。
  • 避免过度绘制:用GPU Profiler看Overdraw区域,把不必要的透明层去掉。

我的建议:如果你用了第三方滤镜库,一定要做性能测试。有些滤镜库为了效果好看,每帧做了七八次纹理采样,GPU直接拉满。我一般会限制滤镜的复杂度,或者提供“性能模式”开关。

28.3 电量优化:让推流更持久

电量优化,说白了就是减少不必要的硬件唤醒。音视频推流本身就很耗电,我们能做的,是把“必要”的耗电降到最低。

28.3.1 网络模块优化

网络传输是耗电大户。RTMP推流需要持续上传数据,如果网络不稳定,频繁重连,电量会哗哗地掉。

  • 合并小包:不要把每帧数据都单独发送。攒够一定大小(比如4KB)再发,减少网络唤醒次数。
  • 自适应码率:网络差的时候,主动降低码率。别让手机在弱网环境下拼命重传,那是最耗电的。
  • 使用Keep-Alive:RTMP本身有连接保持机制,但要注意心跳包的频率。我一般设30秒一次,太频繁反而耗电。

28.3.2 传感器与屏幕优化

推流时,Camera和屏幕是两大耗电源。优化思路如下:

模块 优化措施 预期效果
Camera 降低预览帧率(15fps) 减少ISP处理次数,省电约20%
屏幕 降低屏幕亮度,或使用暗色主题 屏幕是耗电大户,降亮度效果明显
GPS 非必要不开启GPS定位 GPS模块耗电极高,用网络定位替代
WiFi 使用WiFi锁,避免频繁扫描 减少WiFi模块的唤醒次数

注意:不要为了省电而牺牲推流质量。比如把帧率降到5fps,画面卡顿得没法看,用户一样会卸载你的App。省电的前提是用户体验可接受

28.4 知识体系总览

下面这张图,我把本章的核心知识点串了起来。你可以把它当作一个检查清单,做性能优化时对照着看。

RTMP推流性能优化体系 内存泄漏检测 CPU/GPU占用优化 电量优化 LeakCanary / Profiler / MAT MediaCodec回调泄漏修复 Native资源显式释放 对象池复用 / 减少拷贝 硬件编码 / 合理帧率 VBO/VAO / 减少Overdraw 合并小包 / 自适应码率 降低预览帧率 / 屏幕亮度 WiFi锁 / 减少GPS唤醒 核心目标:在保证推流质量的前提下,将资源占用降到最低 三大模块相互影响,需要综合考量,不能孤立优化

28.5 实战避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

我曾经犯过的错:

  • 过度优化帧率:为了省电,把Camera预览帧率降到10fps,结果画面延迟感明显,用户反馈“卡成PPT”。后来我改成动态帧率:静止画面15fps,运动画面30fps,效果好了很多。
  • 忽略GPU温度:只关注CPU占用,没看GPU温度。结果推流20分钟后,GPU降频,画面开始掉帧。后来我加了温度监控,超过阈值就降低特效复杂度。
  • 网络重连太激进:RTMP断线后,我设置1秒重连一次。结果网络不好时,手机一直在重连,电量掉得飞快。后来改成指数退避:1秒、2秒、4秒、8秒...最多30秒一次。

嗯,这一讲的内容就到这里。性能优化没有银弹,需要你根据实际场景不断调优。记住一个原则:先保证功能稳定,再谈性能优化。别为了省5%的电,搞出一堆bug,那就得不偿失了。


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