25、SystemUI的截屏与录屏:ScreenCaptureHelper的实现,MediaProjection的权限申请
截屏和录屏,这两个功能在SystemUI里看似简单,但真要把它做稳定、做流畅,坑其实不少。我最早接触这块是在做某个定制项目时,用户反馈说“录屏有时候点半天没反应”,后来一查,是权限申请流程卡住了。嗯,今天我们就来聊聊SystemUI里截屏录屏的核心——ScreenCaptureHelper和MediaProjection的权限申请。
一、截屏与录屏的整体架构
说白了,Android的截屏和录屏底层都依赖同一个机制——DisplayManager和SurfaceControl。但录屏多了一层MediaProjection,它负责管理屏幕内容的投影权限。
我习惯把整个流程分成三层来看:
- 应用层:SystemUI里的截屏/录屏入口,比如音量键+电源键、快捷开关等。
- 服务层:
ScreenCaptureHelper作为中间协调者,负责调用底层API并处理回调。 - 系统层:
MediaProjectionManager、DisplayManager等系统服务,真正执行截屏或录屏操作。
你想想看,如果这三层之间任何一个环节没配合好,就会出现“点了没反应”或者“权限弹窗一闪而过”的问题。
核心知识点:截屏走的是SurfaceControl.screenshot(),录屏走的是MediaProjection.createVirtualDisplay()。两者路径不同,但权限申请的逻辑有重叠。
二、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 权限申请流程
我习惯把流程拆成四步:
- 创建Intent:调用
MediaProjectionManager.createScreenCaptureIntent()。 - 启动Activity:用
startActivityForResult()启动系统权限确认界面。 - 处理结果:在
onActivityResult()中检查resultCode是否为RESULT_OK。 - 获取令牌:如果授权成功,调用
getMediaProjection()拿到MediaProjection实例。
这里有个坑:getMediaProjection()必须在主线程调用,否则会抛IllegalStateException。我曾经在子线程里调了这个方法,排查了半天才发现问题。
3.2 权限状态管理
| 状态 | 说明 | 处理方式 |
|---|---|---|
| 未申请 | 从未请求过录屏权限 | 正常弹出对话框 |
| 已授权 | 用户点击了“立即开始” | 直接开始录屏 |
| 已拒绝 | 用户点击了“取消” | 提示用户,不再重复弹窗 |
| 永久拒绝 | 用户勾选了“不再询问”并拒绝 | 引导用户去设置中手动开启 |
注意:MediaProjection的权限是一次性的。如果用户授权后,App进程被杀,下次录屏需要重新申请权限。所以不要在全局静态变量里缓存MediaProjection实例,否则进程重启后会拿到一个无效对象。
四、避坑指南与个人经验
做截屏录屏功能,我踩过的坑还真不少。挑几个典型的说说:
- 权限弹窗被遮挡:如果当前有全屏应用(比如游戏),权限弹窗可能被遮挡。解决方案是使用
Dialog主题的Activity,或者用WindowManager添加一个系统级弹窗。 - 录屏时屏幕旋转:录屏过程中如果屏幕旋转,
VirtualDisplay的尺寸需要同步更新。我建议在ConfigurationChanged回调中重新创建VirtualDisplay。 - 内存泄漏:
MediaProjection持有Context引用,如果不及时释放,会导致Activity泄漏。记得在onDestroy()中调用mMediaProjection.stop()。
我曾经遇到一个诡异的问题:录屏文件播放出来是黑屏。查了半天,发现是VirtualDisplay的density参数设置成了0。系统直接报错但不崩溃,录出来的文件就是黑的。嗯,这个坑我记了好几年。
五、总结
截屏和录屏,说白了就是“申请权限 → 获取屏幕内容 → 输出文件”这三步。但每一步都有细节:权限申请要处理好用户拒绝的情况,屏幕内容获取要注意尺寸和旋转,文件输出要选对编码格式。
我个人习惯在SystemUI里用一个ScreenCaptureHelper统一管理这些逻辑,这样不管是快捷开关触发还是按键触发,都走同一套代码。既减少了重复代码,也方便后续维护。
好了,关于截屏录屏的核心实现就聊到这里。如果你在实际开发中遇到其他问题,欢迎一起交流。
公众号:蓝海资料掘金营,微信deep3321