25、SystemUI的截屏与录屏:ScreenCaptureHelper的实现,MediaProjection的权限申请

截屏和录屏,这两个功能在SystemUI里看似简单,但真要把它做稳定、做流畅,坑其实不少。我最早接触这块是在做某个定制项目时,用户反馈说“录屏有时候点半天没反应”,后来一查,是权限申请流程卡住了。嗯,今天我们就来聊聊SystemUI里截屏录屏的核心——ScreenCaptureHelperMediaProjection的权限申请。

一、截屏与录屏的整体架构

说白了,Android的截屏和录屏底层都依赖同一个机制——DisplayManagerSurfaceControl。但录屏多了一层MediaProjection,它负责管理屏幕内容的投影权限。

我习惯把整个流程分成三层来看:

  • 应用层:SystemUI里的截屏/录屏入口,比如音量键+电源键、快捷开关等。
  • 服务层ScreenCaptureHelper作为中间协调者,负责调用底层API并处理回调。
  • 系统层MediaProjectionManagerDisplayManager等系统服务,真正执行截屏或录屏操作。

你想想看,如果这三层之间任何一个环节没配合好,就会出现“点了没反应”或者“权限弹窗一闪而过”的问题。

核心知识点:截屏走的是SurfaceControl.screenshot(),录屏走的是MediaProjection.createVirtualDisplay()。两者路径不同,但权限申请的逻辑有重叠。

截屏与录屏架构图 应用层 SystemUI 入口 快捷开关 / 按键 服务层 ScreenCaptureHelper 权限协调 / 回调处理 系统层 MediaProjectionManager DisplayManager 用户触发 协调调度 系统执行 权限申请流程 1. 应用调用 createScreenCaptureIntent() 2. 系统弹出权限确认对话框 3. 用户授权后获取 MediaProjection 令牌 4. 通过令牌创建 VirtualDisplay 开始录屏

二、ScreenCaptureHelper 的实现

ScreenCaptureHelper这个类,说白了就是SystemUI里截屏录屏的“调度中心”。它不直接干活,但负责把各个模块串起来。我见过有些团队把截屏逻辑直接写在Activity里,结果代码又臭又长。用Helper类封装一下,清爽很多。

2.1 核心职责

  • 权限管理:统一处理MediaProjection的申请和释放。
  • 回调分发:截屏成功或失败后,通知UI层更新。
  • 状态维护:记录当前是否正在截屏/录屏,防止重复操作。

我记得有一次,用户连续快速按了三次截屏键,结果系统弹了三个Toast。后来我在Helper里加了个isCapturing的布尔锁,问题就解决了。

2.2 关键代码片段

public class ScreenCaptureHelper {
    private static final String TAG = "ScreenCaptureHelper";
    private MediaProjectionManager mMediaProjectionManager;
    private MediaProjection mMediaProjection;
    private boolean isCapturing = false;

    public void startScreenCapture(Activity activity) {
        if (isCapturing) {
            Log.w(TAG, "已经在截屏中,忽略重复请求");
            return;
        }
        Intent intent = mMediaProjectionManager.createScreenCaptureIntent();
        activity.startActivityForResult(intent, REQUEST_CODE_CAPTURE);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE_CAPTURE) {
            if (resultCode == Activity.RESULT_OK) {
                mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
                isCapturing = true;
                // 开始截屏或录屏
                performCapture();
            } else {
                // 用户拒绝了权限
                onCaptureFailed("用户拒绝了截屏权限");
            }
        }
    }
}

小技巧createScreenCaptureIntent()返回的Intent其实是一个PendingIntent的包装,系统会用它来弹出权限确认对话框。不要试图绕过这个对话框,否则会直接抛SecurityException。

三、MediaProjection 的权限申请

录屏权限和普通权限不一样。普通权限比如相机、位置,在Manifest里声明一下就行。但录屏权限必须由用户每次手动确认。为什么?因为屏幕内容太敏感了——你想想看,如果某个App能偷偷录屏,那用户的密码、聊天记录全暴露了。

3.1 权限申请流程

我习惯把流程拆成四步:

  1. 创建Intent:调用MediaProjectionManager.createScreenCaptureIntent()
  2. 启动Activity:用startActivityForResult()启动系统权限确认界面。
  3. 处理结果:在onActivityResult()中检查resultCode是否为RESULT_OK
  4. 获取令牌:如果授权成功,调用getMediaProjection()拿到MediaProjection实例。

这里有个坑:getMediaProjection()必须在主线程调用,否则会抛IllegalStateException。我曾经在子线程里调了这个方法,排查了半天才发现问题。

3.2 权限状态管理

状态 说明 处理方式
未申请 从未请求过录屏权限 正常弹出对话框
已授权 用户点击了“立即开始” 直接开始录屏
已拒绝 用户点击了“取消” 提示用户,不再重复弹窗
永久拒绝 用户勾选了“不再询问”并拒绝 引导用户去设置中手动开启

注意MediaProjection的权限是一次性的。如果用户授权后,App进程被杀,下次录屏需要重新申请权限。所以不要在全局静态变量里缓存MediaProjection实例,否则进程重启后会拿到一个无效对象。

四、避坑指南与个人经验

做截屏录屏功能,我踩过的坑还真不少。挑几个典型的说说:

  • 权限弹窗被遮挡:如果当前有全屏应用(比如游戏),权限弹窗可能被遮挡。解决方案是使用Dialog主题的Activity,或者用WindowManager添加一个系统级弹窗。
  • 录屏时屏幕旋转:录屏过程中如果屏幕旋转,VirtualDisplay的尺寸需要同步更新。我建议在ConfigurationChanged回调中重新创建VirtualDisplay
  • 内存泄漏MediaProjection持有Context引用,如果不及时释放,会导致Activity泄漏。记得在onDestroy()中调用mMediaProjection.stop()

我曾经遇到一个诡异的问题:录屏文件播放出来是黑屏。查了半天,发现是VirtualDisplaydensity参数设置成了0。系统直接报错但不崩溃,录出来的文件就是黑的。嗯,这个坑我记了好几年。

五、总结

截屏和录屏,说白了就是“申请权限 → 获取屏幕内容 → 输出文件”这三步。但每一步都有细节:权限申请要处理好用户拒绝的情况,屏幕内容获取要注意尺寸和旋转,文件输出要选对编码格式。

我个人习惯在SystemUI里用一个ScreenCaptureHelper统一管理这些逻辑,这样不管是快捷开关触发还是按键触发,都走同一套代码。既减少了重复代码,也方便后续维护。

好了,关于截屏录屏的核心实现就聊到这里。如果你在实际开发中遇到其他问题,欢迎一起交流。


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