28、性能优化:内存管理、CPU占用、电池消耗

移动端做WebRTC,说白了就是「戴着镣铐跳舞」。

你想想看,手机就那么点内存,CPU还得省着用,电池更是金贵。PC上跑得飞快的代码,搬到手机上可能直接闪退、发烫、掉电如流水。我早期做的一个视频通话项目,在iPhone 6上测试,通话5分钟手机能煎鸡蛋——嗯,那滋味真不好受。

今天咱们就聊聊,怎么让WebRTC在移动端跑得又稳又省。

核心原则:移动端性能优化的本质,是在「用户体验」和「资源消耗」之间找平衡。不是一味省电,而是该省的时候省,该用的时候用。

移动端WebRTC性能优化三大维度 内存管理 • 视频帧缓冲池复用 • 编码器/解码器实例管理 • 音频缓冲区大小控制 • 纹理内存释放 • 弱引用避免泄漏 • 低内存时降级策略 • 及时释放无用资源 CPU占用 • 分辨率动态调整 • 帧率自适应控制 • 编解码硬件加速 • 网络线程分离 • 渲染频率控制 • 降噪算法轻量化 • 定时器合并 电池消耗 • 屏幕常亮控制 • 网络唤醒策略 • 后台任务节流 • 传感器采样降频 • 音频播放省电模式 • 摄像头功耗管理 • 网络切换降功耗 三者相互影响:内存泄漏 → CPU升高 → 电池加速消耗

一、内存管理:别让App偷偷「长胖」

移动端内存是硬约束。iOS有jetsam机制,内存超了直接杀进程,连个招呼都不打。Android虽然好一点,但OOM也是家常便饭。

1.1 视频帧缓冲池

WebRTC里视频帧是高频对象。每秒钟30帧,每帧都是YUV数据或纹理。如果每帧都new一个对象,GC会频繁触发,卡顿就来了。

我的做法:用对象池复用VideoFrame。提前分配好N个帧缓冲区,用的时候取,用完归还。我见过一个项目,没做池化,通话10分钟内存涨了80MB——全是帧对象没释放。

// 伪代码:视频帧缓冲池
class VideoFramePool {
    private val pool = ArrayDeque<VideoFrame>(MAX_POOL_SIZE)
    
    fun acquire(): VideoFrame {
        return pool.removeFirstOrNull() ?: createNewFrame()
    }
    
    fun release(frame: VideoFrame) {
        if (pool.size < MAX_POOL_SIZE) {
            frame.clearData()  // 只清数据,不释放内存
            pool.addLast(frame)
        } else {
            frame.release()    // 池满了才真正释放
        }
    }
}

1.2 编码器/解码器实例管理

硬编解码器是稀缺资源。iOS的VTCompressionSession、Android的MediaCodec,创建和销毁都很重。

  • 不要频繁创建销毁:切换摄像头时,复用编解码器实例,而不是重建。
  • 及时释放:通话结束时,一定要调用release()。我曾经踩过一个坑——Android上MediaCodec没释放,导致后续所有通话都无法硬编。
  • 控制并发数:同时最多保留1个编码器+1个解码器。多路通话时,按需激活/休眠。

1.3 音频缓冲区管理

音频数据虽然小,但积少成多。10ms一包,一秒钟100包,如果不控制缓冲区大小,内存也会涨。

注意:音频JitterBuffer默认会缓存200-300ms的数据。如果网络抖动不大,可以调小到100ms,省内存也降低延迟。我一般在项目里加一个自适应策略:网络好时缓存50ms,差时扩大到200ms。

1.4 低内存时的降级策略

App应该监听系统低内存警告。收到警告后:

  1. 降低视频分辨率(720p → 480p)
  2. 降低帧率(30fps → 15fps)
  3. 关闭视频预览(只传不显)
  4. 释放非关键资源(如历史帧缓存)

经验之谈:iOS上收到didReceiveMemoryWarning后,如果不做降级,大概率在3秒内被kill。Android上onTrimMemory(TRIM_MEMORY_RUNNING_CRITICAL)也是同样的信号。别犹豫,立刻降级。

二、CPU占用:别让手机「发烧」

CPU占用高了,手机发烫,然后降频,然后卡顿,然后用户骂娘。这是个恶性循环。

2.1 分辨率与帧率自适应

别傻乎乎地一直传1080p 30fps。要根据网络和CPU负载动态调整。

// WebRTC内置的CPU自适应开关
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
// 开启CPU自适应
options.disable_encryption = false;  // 保持加密
// 设置自适应参数
mediaConstraints.mandatory.add(
    new MediaConstraints.KeyValuePair(
        "googCpuOveruseDetection", "true"
    )
);
mediaConstraints.mandatory.add(
    new MediaConstraints.KeyValuePair(
        "googCpuOveruseEncodeUsage", "true"
    )
);

WebRTC内部有个CPU过载检测器。它会监控编码耗时和CPU占用率。一旦超标,自动降分辨率或帧率。我个人习惯把这个阈值调低一点——默认是80%,我改成70%,让降级来得更早一些。

2.2 硬件编解码优先

软编软解是CPU杀手。同样是H.264编码,硬编比软编省电60%以上。

编码方式 CPU占用 功耗 发热
硬件编码 5-10% 微热
软件编码 30-50% 烫手

避坑指南:Android上硬编兼容性是个大问题。有些国产手机硬编出来的码流花屏。我曾经在某个项目里加了硬编回退逻辑:先尝试硬编,如果前10帧编码失败或质量差,自动切到软编。用户无感知,但稳定多了。

2.3 渲染频率控制

UI渲染也是CPU大户。WebRTC默认每帧都渲染,但很多时候没必要。

  • 远端视频:如果对方画面变化不大,可以降到15fps渲染
  • 本地预览:可以降到10fps,反正自己看
  • 画中画:小窗口用更低帧率

嗯,这里要注意:渲染降频不等于编码降频。编码还是30fps,只是显示的时候跳帧。这样既保证了传输质量,又省了CPU。

2.4 网络线程与UI线程分离

WebRTC默认的网络IO和编解码都在独立线程。但有些开发者图省事,把回调扔到主线程处理。这是大忌。

// 错误做法:在主线程处理视频帧
videoTrack.addSink { frame ->
    runOnUiThread {
        renderer.renderFrame(frame)  // 阻塞主线程!
    }
}

// 正确做法:在渲染线程处理
val renderThread = HandlerThread("VideoRender")
renderThread.start()
val renderHandler = Handler(renderThread.looper)
videoTrack.addSink { frame ->
    renderHandler.post {
        renderer.renderFrame(frame)
    }
}

三、电池消耗:让App「省着点花」

电池优化往往被忽视。但用户对掉电速度非常敏感。我见过一个App,开视频通话1小时掉电60%,直接被用户打一星。

3.1 屏幕常亮控制

视频通话时,屏幕常亮是必要的。但别用最高亮度。我建议:

  • 通话开始时,获取当前屏幕亮度,降低20%
  • 通话结束时,恢复原始亮度
  • 如果用户切到后台,立即释放屏幕常亮锁

3.2 网络唤醒策略

WiFi和蜂窝网络的功耗差别很大。WiFi下可以保持较高码率,蜂窝网络下要降码率。

我的经验:监听网络类型变化。切到4G/5G时,自动把视频码率从2Mbps降到1Mbps。切回WiFi时再恢复。别小看这个调整,能省15%的电量。

3.3 传感器采样降频

很多App在视频通话时还开着加速度计、陀螺仪做防抖或旋转。这些传感器很耗电。

  • 加速度计:默认100Hz采样,通话时降到20Hz
  • 陀螺仪:不用就关掉
  • 摄像头:不用前置时,关闭摄像头电源(不只是停止预览)

3.4 音频播放省电模式

音频播放也有讲究。用扬声器外放比用听筒费电。如果检测到用户用听筒,可以降低音频采样率。

// 根据音频输出设备调整参数
if (audioDevice == AudioDevice.EARPIECE) {
    // 听筒模式:降低采样率到16kHz
    audioTrack.setSampleRate(16000)
} else {
    // 扬声器模式:保持48kHz
    audioTrack.setSampleRate(48000)
}

3.5 后台任务节流

App切到后台后,应该大幅降低资源消耗:

  1. 停止视频编码(只传音频)
  2. 关闭摄像头
  3. 降低音频码率
  4. 延长心跳间隔(从5秒一次延长到30秒一次)

注意:iOS后台保活时间有限(约30秒)。如果要做后台通话,必须用VoIP Push。Android上也要用前台服务,否则系统会杀进程。别指望后台还能保持视频通话——那是不可能的。

四、综合优化清单

最后,我整理了一份检查清单。每次上线前,对着过一遍:

类别 检查项 优先级
内存 视频帧是否使用对象池 P0
内存 编解码器是否及时释放 P0
内存 是否监听低内存警告并降级 P1
CPU 是否开启CPU自适应 P0
CPU 是否优先使用硬编解码 P0
CPU 渲染线程是否独立 P1
电池 屏幕亮度是否自适应 P1
电池 网络切换时是否调整码率 P1
电池 后台是否停止视频传输 P0

说实话,性能优化没有银弹。每个项目的情况都不一样。但记住一个原则:用多少资源,取决于用户当前需要多少,而不是你能给多少。

我做了这么多年WebRTC,最大的感悟就是:移动端优化,七分在架构设计,三分在调参。架构设计好了,后面调参就是锦上添花。架构设计烂了,再怎么调也是杯水车薪。

嗯,今天就聊到这儿。这些经验都是真金白银踩坑踩出来的,希望对你有帮助。

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