多窗口与画中画:分屏适配、PiP生命周期管理、多实例播放器资源管控

多窗口和画中画,说白了就是让用户一边看视频一边干别的事。这个需求在平板和折叠屏上越来越常见。我最早接触这个特性是在Android 7.0刚推出分屏的时候,那时候踩了不少坑。今天咱们就把这块彻底讲透。

分屏适配的核心要点

分屏模式下,你的播放器窗口会被压缩到屏幕的一半甚至更小。这时候最直接的问题就是——UI布局崩了。我个人习惯的做法是,在布局文件中使用android:minWidthandroid:minHeight来限制最小尺寸,而不是依赖代码动态调整。

核心原则:分屏不是异常状态,而是Android多任务的标准形态。你的播放器必须能在任意尺寸下正常工作。

我在项目中遇到过一个问题:分屏后视频画面被拉伸变形。原因很简单——SurfaceView的宽高比没有跟随窗口变化。解决方案是监听onMultiWindowModeChanged回调,重新计算视频渲染尺寸。

@Override
public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
    super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
    if (isInMultiWindowMode) {
        // 获取当前可用窗口尺寸
        Rect bounds = newConfig.windowConfiguration.getBounds();
        // 重新计算视频渲染区域,保持宽高比
        adjustVideoRenderSize(bounds.width(), bounds.height());
    }
}

嗯,这里要注意一点:onMultiWindowModeChanged在Android 7.0以上才支持。如果你要兼容更老的版本,可以用onConfigurationChanged配合isInMultiWindowMode()来判断。

画中画生命周期管理

画中画(PiP)和分屏最大的区别在于——PiP模式下你的Activity可能随时被销毁。为什么?因为系统需要回收资源给前台应用。你想想看,用户在看视频的同时刷微博,你的播放器在PiP窗口里,如果系统内存紧张,你的Activity就会被杀掉。

我曾经踩过一个坑:用户从PiP模式恢复时,播放器黑屏了。排查后发现是onPictureInPictureModeChanged回调里没有正确处理Surface的重新创建。正确的做法是这样的:

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    if (isInPictureInPictureMode) {
        // 进入PiP模式,暂停UI更新,但保持播放
        pauseUIUpdates();
        // 释放不必要的资源,比如大图缓存
        releaseUnnecessaryResources();
    } else {
        // 退出PiP模式,恢复UI
        resumeUIUpdates();
        // 重新初始化Surface
        reinitializeSurface();
    }
}

个人经验:在PiP模式下,不要做任何UI动画,也不要在主线程执行耗时操作。系统会给PiP窗口分配极低的CPU时间片,你稍微卡一下,用户就能感觉到掉帧。

还有一个容易被忽略的点:onUserLeaveHint回调。当用户按下Home键或者切换到其他应用时,这个回调会被触发。我建议在这里提前准备好PiP模式的参数,而不是等到系统真正进入PiP时才去准备。

多实例播放器资源管控

多窗口模式下,用户可能同时打开两个视频应用,或者同一个应用打开两个播放器实例。这时候资源管控就成了大问题。说白了,你的播放器不能独占硬件解码器,也不能无限制地申请内存。

我见过最夸张的情况:一个视频应用在分屏模式下同时播放两个4K视频,结果手机直接卡死。为什么?因为硬件解码器只有一个,两个播放器都在抢,最终谁都没拿到。

避坑指南:我曾经在项目中遇到一个bug——分屏模式下,两个播放器实例同时使用MediaCodec,导致其中一个解码失败。后来发现是MediaCodec的异步模式在多实例下存在竞态条件。解决方案是使用同步模式,或者为每个实例分配独立的HandlerThread。

资源管控的核心策略可以总结为三点:

  • 解码器复用:如果两个播放器播放的是同一种编码格式,尽量复用同一个解码器实例。这需要你实现一个解码器池。
  • 内存分级:根据窗口大小动态调整缓冲区大小。PiP模式下,缓冲区可以缩小到正常模式的1/4。
  • 优先级调度:前台窗口的播放器拥有更高的资源优先级。后台窗口的播放器可以降级到软解或者降低帧率。
public class PlayerResourceManager {
    private static final int MAX_DECODER_INSTANCES = 2;
    private final Map<String, MediaCodec> decoderPool = new HashMap<>();
    
    public MediaCodec acquireDecoder(String mimeType) {
        if (decoderPool.size() >= MAX_DECODER_INSTANCES) {
            // 释放优先级最低的实例
            releaseLowestPriorityDecoder();
        }
        MediaCodec decoder = MediaCodec.createDecoderByType(mimeType);
        decoderPool.put(mimeType + "_" + System.currentTimeMillis(), decoder);
        return decoder;
    }
    
    public void releaseDecoder(MediaCodec decoder) {
        decoder.stop();
        decoder.release();
        decoderPool.values().remove(decoder);
    }
}

这个代码只是一个简化版本。实际项目中,你还需要考虑解码器的状态管理、异常恢复、以及不同厂商的兼容性问题。嗯,这里面的坑确实不少。

知识体系总览

下面这张图总结了多窗口与画中画的核心知识点。我把它画成了流程图,方便你理解各个模块之间的关系。

多窗口与画中画知识体系 分屏适配 UI布局 · 尺寸变化 PiP生命周期 状态回调 · 资源释放 多实例管控 解码器池 · 内存分级 onMultiWindowModeChanged 最小尺寸限制 onPictureInPictureModeChanged Surface重新创建 解码器池管理 优先级调度策略 核心目标:用户在多任务场景下获得流畅、稳定的播放体验

这张图把三个核心模块串起来了。分屏适配解决的是UI层面的问题,PiP生命周期管理解决的是状态切换的问题,多实例资源管控解决的是底层硬件资源竞争的问题。三者缺一不可。

最后说一句:多窗口和画中画的适配,本质上是对Android多任务机制的深度理解。不要把它当成一个「额外功能」,而是作为播放器架构的基础能力来设计。这样你的代码才能在各种场景下都稳定运行。

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