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 知识体系总览
下面这张图,我把本章的核心知识点串了起来。你可以把它当作一个检查清单,做性能优化时对照着看。
28.5 实战避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
我曾经犯过的错:
- 过度优化帧率:为了省电,把Camera预览帧率降到10fps,结果画面延迟感明显,用户反馈“卡成PPT”。后来我改成动态帧率:静止画面15fps,运动画面30fps,效果好了很多。
- 忽略GPU温度:只关注CPU占用,没看GPU温度。结果推流20分钟后,GPU降频,画面开始掉帧。后来我加了温度监控,超过阈值就降低特效复杂度。
- 网络重连太激进:RTMP断线后,我设置1秒重连一次。结果网络不好时,手机一直在重连,电量掉得飞快。后来改成指数退避:1秒、2秒、4秒、8秒...最多30秒一次。
嗯,这一讲的内容就到这里。性能优化没有银弹,需要你根据实际场景不断调优。记住一个原则:先保证功能稳定,再谈性能优化。别为了省5%的电,搞出一堆bug,那就得不偿失了。
公众号:蓝海资料掘金营,微信deep3321