23、性能优化:内存优化、CPU占用控制、渲染帧率优化、电量消耗优化

做视频会议APP,最怕什么?

用户打着打着,手机发烫了。或者后台切回来,画面卡成PPT。再或者,开完会手机没电了。

这些我都遇到过。有一次线上演示,客户的低端机直接闪退,场面一度非常尴尬。从那以后,性能优化就成了我开发流程里优先级最高的事。

今天我们就来聊聊,Android平台上WebRTC视频会议APP的四大性能优化方向。说白了,就是让APP跑得稳、不卡顿、不费电、不崩。

核心思路:性能优化不是一次性工作,而是贯穿开发全流程的持续改进。从架构设计到代码实现,再到线上监控,每一步都要有优化意识。

WebRTC性能优化 内存优化 CPU占用控制 渲染帧率优化 电量消耗优化 视频帧复用 对象池化 Bitmap回收 编码器选择 分辨率自适应 帧率控制 SurfaceView 硬件加速 帧率监控 网络策略 休眠策略

一、内存优化:别让APP成为内存杀手

视频会议APP的内存消耗,主要来自三块:视频帧数据、音频缓冲区、以及各种编解码器实例。我见过最夸张的情况,一个四路视频通话,内存直接飙到500MB以上。

1.1 视频帧复用

WebRTC内部,每一帧视频数据都是通过VideoFrame对象传递的。如果你每次收到帧都new一个新对象,GC会频繁触发,卡顿就来了。

// 错误做法:每次都创建新对象
VideoFrame frame = new VideoFrame(buffer, rotation, timestampNs);

// 正确做法:复用帧对象
private VideoFrame.I420Buffer reusableBuffer;

public void onFrame(VideoFrame frame) {
    // 复用buffer,减少内存分配
    VideoFrame.I420Buffer buffer = frame.getBuffer().toI420();
    // 处理帧逻辑...
    buffer.release(); // 记得释放
}

我的经验:在PeerConnection的VideoSink中,尽量使用setVideoFramePoolSize()设置帧池大小。我一般设为3-5,既能保证流畅,又不会浪费内存。

1.2 对象池化

编解码过程中,会频繁创建和销毁各种临时对象。比如编码后的数据包、RTP包等。用对象池来管理,能显著减少GC压力。

public class PacketPool {
    private final Queue<EncodedImage> pool = new ArrayDeque<>(50);
    
    public EncodedImage obtain() {
        EncodedImage image = pool.poll();
        return image != null ? image : new EncodedImage();
    }
    
    public void recycle(EncodedImage image) {
        if (pool.size() < 50) {
            image.buffer = null; // 清空引用
            pool.offer(image);
        }
    }
}

1.3 Bitmap回收

如果你需要显示视频帧的缩略图或者做截图,记得及时回收Bitmap。Android 8.0之后Bitmap内存虽然在native堆,但依然受虚拟内存限制。

注意:不要在渲染线程中做Bitmap的decode操作。我曾经踩过这个坑,导致UI线程卡顿,画面掉帧严重。把解码操作放到子线程,用Handler发回主线程显示。

二、CPU占用控制:让手机不再发烫

CPU占用高,直接导致两个问题:手机发热、电池续航下降。视频会议中,编解码是CPU消耗的大头。

2.1 编码器选择

Android平台支持硬件编码器。能用硬编就别用软编。硬编的功耗只有软编的1/3左右。

编码器类型 CPU占用 功耗 适用场景
H.264 硬编 低(10-20%) 大多数Android设备
H.264 软编 高(40-60%) 兼容性兜底
VP8 硬编 中(20-30%) 部分中高端设备
VP9 软编 很高(60%+) 很高 不推荐移动端使用
// 优先使用硬件编码器
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(
    new MediaConstraints.KeyValuePair(
        "googUseHardwareVideoEncoder", "true"
    )
);
constraints.mandatory.add(
    new MediaConstraints.KeyValuePair(
        "googUseHardwareVideoDecoder", "true"
    )
);

2.2 分辨率自适应

别傻傻地一直用1080p。网络差或者CPU高的时候,主动降分辨率。WebRTC自带的VideoSource.adaptOutputFormat()就是干这个的。

// 根据CPU负载动态调整分辨率
public void onCpuUsageHigh() {
    videoSource.adaptOutputFormat(
        640, 480,  // 目标分辨率
        15,        // 目标帧率
        null       // 回调
    );
}

public void onCpuUsageLow() {
    videoSource.adaptOutputFormat(
        1280, 720,
        30,
        null
    );
}

避坑指南:我曾经在低端机上用1080p硬编,结果编码器直接崩溃。后来加了分辨率降级策略,当CPU占用超过70%时,自动降到720p甚至480p。效果立竿见影。

2.3 帧率控制

不是所有场景都需要30fps。屏幕共享、文档展示这些场景,15fps完全够用。降低帧率能直接减少编码器的计算量。

// 屏幕共享场景,降低帧率
VideoSource screenSource = factory.createVideoSource(false);
screenSource.adaptOutputFormat(
    1280, 720,
    15,  // 15fps
    null
);

三、渲染帧率优化:告别卡顿画面

渲染卡顿,用户感知最明显。我调试过很多次,发现大部分渲染问题都出在View的选择和线程模型上。

3.1 用SurfaceView,别用TextureView

SurfaceView有独立的渲染层,不占用主线程的绘制时间。TextureView虽然支持动画和变换,但会占用GPU资源,导致帧率下降。

对比项 SurfaceView TextureView
渲染性能 高(独立窗口) 中(共享窗口)
动画支持
内存占用
推荐场景 视频渲染 需要动画的UI
// 推荐:使用SurfaceView渲染视频
SurfaceViewRenderer renderer = new SurfaceViewRenderer(context);
renderer.setMirror(true);
renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);

3.2 开启硬件加速

在AndroidManifest中为Activity开启硬件加速,能显著提升渲染性能。

<activity
    android:name=".VideoCallActivity"
    android:hardwareAccelerated="true" />

3.3 帧率监控

我习惯在开发阶段开启帧率监控,用Choreographer或者FrameMetrics来检测掉帧情况。

// 使用FrameMetrics监控渲染性能
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    window.addOnFrameMetricsAvailableListener(
        (window, frameMetrics, dropCountSinceLastInvocation) -> {
            long frameDuration = frameMetrics.getMetric(
                FrameMetrics.TOTAL_DURATION
            );
            if (frameDuration > 16_000_000) { // 超过16ms就是掉帧
                Log.w("FrameDrop", "掉帧了,耗时: " + frameDuration / 1_000_000 + "ms");
            }
        },
        new Handler(Looper.getMainLooper())
    );
}

注意:帧率监控代码只在debug版本开启。线上版本不要加,会影响性能。我一般用BuildConfig.DEBUG来控制。

四、电量消耗优化:让会议撑到最后一刻

电量优化,说白了就是让APP在后台时少干活,在前台时高效干活。

4.1 网络策略优化

WebRTC默认的网路探测和保活机制,在弱网环境下会频繁发送STUN请求,非常耗电。我建议根据网络状态动态调整探测频率。

// 根据网络类型调整STUN探测间隔
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
    Context.CONNECTIVITY_SERVICE
);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork != null) {
    if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
        // WiFi下,每30秒探测一次
        stunInterval = 30_000;
    } else {
        // 移动网络下,每60秒探测一次
        stunInterval = 60_000;
    }
}

4.2 休眠策略

当用户切到后台或者屏幕关闭时,主动降低视频质量,甚至暂停视频流。音频保持即可。

// 监听屏幕状态
private BroadcastReceiver screenReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
            // 屏幕关闭,降低视频质量
            videoSource.adaptOutputFormat(320, 240, 10, null);
            // 或者直接停止视频发送
            localVideoTrack.setEnabled(false);
        } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
            // 屏幕亮起,恢复视频
            videoSource.adaptOutputFormat(1280, 720, 30, null);
            localVideoTrack.setEnabled(true);
        }
    }
};

我的习惯:在后台运行时,我会把视频编码器切换到低功耗模式。Android 10以上支持MediaCodec的低功耗配置,能省电30%左右。

4.3 避免唤醒锁滥用

很多开发者为了保持网络连接,会持有一个WakeLock。但如果不及时释放,电量会快速耗尽。我建议只在需要发送关键数据时短暂持有。

// 正确使用WakeLock
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
    PowerManager.PARTIAL_WAKE_LOCK,
    "webrtc:keep_alive"
);

// 只在发送关键信令时持有
wakeLock.acquire(5000); // 最多持有5秒
// 发送信令...
wakeLock.release();

嗯,以上就是性能优化的四个核心方向。每个方向都有很多细节,但抓住关键点,就能解决80%的问题。我个人觉得,性能优化没有银弹,最好的方法就是持续监控、持续改进。在开发阶段就把性能指标卡死,别等到线上出问题再救火。

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