20. 多窗口与画中画支持:多窗口生命周期、PIP模式下的渲染与同步

多窗口和画中画(PIP),这两个功能现在已经是Android系统的标配了。我记得刚做Stagefright那会儿,Android还只有单窗口模式,后来Android 7.0引入多窗口,再到Android 8.0正式支持PIP,每一次改动都让多媒体栈经历了一次不小的重构。

说白了,多窗口和PIP的核心挑战就两个:生命周期怎么管渲染怎么同步。今天我们就来拆解这两个问题。

20.1 多窗口下的生命周期变化

先聊聊多窗口。当你的视频应用进入分屏模式,Activity的生命周期会怎么走?

嗯,这里有个关键点:多窗口模式下,Activity不会被销毁。它只是从“前台可见”变成了“部分可见”。但Stagefright的MediaPlayer呢?它可不管这些,它只认Surface的状态。

我在项目中遇到过一个问题:分屏后视频播放突然卡顿。排查了半天,发现是Surface被系统回收了,但MediaPlayer还在往上面送数据。这就是生命周期没对齐的典型表现。

核心原则: 多窗口模式下,MediaPlayer的生命周期必须跟随Surface的可用性,而不是Activity的可见性。

具体来说,你需要监听以下几个回调:

  • onMultiWindowModeChanged() — 进入/退出多窗口
  • onSurfaceChanged() — Surface尺寸变化
  • onSurfaceDestroyed() — Surface被销毁

我个人习惯的做法是:在onMultiWindowModeChanged()中暂停播放,等Surface稳定后再恢复。你想想看,如果Surface尺寸从1080p突然变成540p,解码器输出的分辨率没跟上,那画面不就撕裂了吗?

20.2 PIP模式下的渲染挑战

画中画模式比多窗口更特殊。PIP窗口通常只有200dp左右,但视频源可能是4K的。这里就涉及到一个问题:解码器要不要降分辨率?

我的建议是:不要降。为什么?

第一,降分辨率需要重新配置解码器,这个操作很重,会导致明显的卡顿。第二,用户随时可能把PIP窗口拖回全屏,如果解码器还在低分辨率模式,那全屏后的画质就惨不忍睹了。

那怎么办?让SurfaceFlinger来做缩放。解码器输出全分辨率帧,SurfaceFlinger负责缩放到PIP窗口大小。这样既保证了画质,又避免了解码器重启。

小技巧: 在PIP模式下,可以降低解码器的帧率。比如从60fps降到30fps,用户几乎感知不到,但能省不少电。我曾经在某个平板上做过测试,帧率减半后功耗降低了约35%。

20.3 渲染同步:一个容易被忽视的坑

多窗口和PIP模式下,渲染同步是个大问题。为什么?因为多个窗口可能共享同一个显示管道

举个例子:你的视频应用在分屏左侧播放,右侧是另一个应用。两个应用都在往SurfaceFlinger提交Buffer。如果同步没做好,就会出现“掉帧”或“画面撕裂”。

Stagefright是怎么处理的?它依赖BufferQueue的同步机制。每个Surface对应一个BufferQueue,生产者(MediaCodec)和消费者(SurfaceFlinger)通过dequeue/queue操作来同步。

但这里有个坑:PIP模式下,BufferQueue的深度可能不够。全屏时你可能用了3个Buffer,但PIP窗口小,SurfaceFlinger处理得快,3个Buffer就浪费了。我建议动态调整BufferQueue的深度:

// 全屏模式
SurfaceHolder holder = getSurfaceHolder();
holder.setBufferSize(3);

// PIP模式
holder.setBufferSize(2);  // 减少Buffer数量,降低延迟

嗯,这里要注意:setBufferSize()不是所有设备都支持。我在某款低端机上就踩过这个坑,调用后直接崩溃。所以一定要加try-catch,或者先检查API版本。

20.4 知识体系总览

下面这张图总结了多窗口和PIP的核心逻辑:

多窗口与PIP核心逻辑 应用层 生命周期管理 onMultiWindowModeChanged() Surface状态监听 渲染与同步 BufferQueue同步 SurfaceFlinger缩放 Stagefright多媒体栈 MediaCodec + MediaPlayer + NuPlayer

20.5 避坑指南

最后分享几个我踩过的坑:

坑1:PIP模式下不要调用seek()
我曾经在PIP窗口里做seek操作,结果画面卡住了。原因是PIP模式下解码器处于低功耗状态,seek会触发解码器重启,但SurfaceFlinger那边还在等Buffer。解决方案:PIP模式下禁用seek,或者先退出PIP再seek。
坑2:多窗口切换时不要立即释放资源
用户从分屏切回全屏,中间可能只有几百毫秒。如果你在onMultiWindowModeChanged()里直接释放解码器,那切回来时就得重新创建,卡顿感很明显。我建议用Handler.postDelayed()延迟释放,给用户一个“反悔”的时间。
坑3:PIP窗口的触摸事件
PIP窗口默认只响应拖动和双击。如果你想支持更多手势(比如滑动调节音量),需要自己实现onTouchEvent()。但注意:PIP窗口的触摸区域很小,手势识别要做得灵敏一些。

好了,多窗口和PIP的内容就聊到这里。核心就是记住:生命周期跟着Surface走,渲染同步靠BufferQueue。下次遇到多窗口下的播放问题,先检查这两个地方,基本能解决80%的问题。


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