做个屏幕录制器,说难不难,但真要做得顺手、录出来不出问题,还是有一堆细节要处理的。尤其是把它当成课程设计、毕业设计,或者练手项目,很多人都卡在“源码能跑,但自己写就崩”这个坎上。这篇文章我就拿Android Studio + Java这套最经典、也最适合学习的组合,把屏幕录制器从原理到源码、从跑到调优,完整拆开来讲。
先说明一点,Android 官方的 MediaProjection API 是目前做屏幕录制绕不开的底层方案,凡是你能在应用商店看到的录屏软件,底层基本都离不开它。理解了这个 API 的工作方式,后面不管是做直播推流、录屏剪辑,还是做在线教学工具,都是一通百通。适合人群很明确:正在学 Android 的在校生、准备做毕业设计的老铁,以及想自己动手写一个录屏工具但又不想直接抄别人代码的开发者。
1. 项目整体设计与实现思路拆解
1.1 屏幕录制器的核心原理
屏幕录制说白了就是三个环节:拿到屏幕画面、编码、写成文件。Android 系统为了安全考虑,不允许普通应用直接截取其他应用的画面,所以它提供了一个专门的通道——MediaProjection。这套机制的逻辑很清晰:用户主动点击“允许”授权后,系统会返回一个虚拟屏幕的数据源,应用拿到这个数据源,把手机屏幕的画面实时投到一个看不见的虚拟显示器上,再把这个显示器上的数据和麦克风的声音一起喂给编码器,最后输出成 MP4 文件。
很多人第一次接触会觉得很玄乎,其实你可以把它理解成“手机内部有一个隐形的大电视”。MediaProjection 创建出的 VirtualDisplay 就是这台大电视,屏幕上的每一帧画面都会在电视上实时播放,应用做的只不过是在电视旁边架了一台录像机(MediaRecorder),按帧录制而已。这个类比贯穿整个开发过程,你只需要记住:VirtualDisplay 管画面,MediaRecorder 管编码和封装,AudioRecord 管声音。
1.2 为什么选 Java 而不是 Kotlin
现在新项目几乎都推荐 Kotlin,但我在这个项目上依旧选了 Java,而且我建议刚入门的朋友也先从 Java 走一遍。理由很简单,屏幕录制器涉及到的关键代码——MediaProjection、VirtualDisplay、MediaRecorder——在官方文档和大量老项目中的示例都是 Java 写的,遇到问题去搜索时,Java 版本能找到的答案多得多。
另外 Java 的泛型和反射机制相对直观,调试时能更清晰地看到类型转换的细节,这对理解 MediaProjection 回调中返回的 Intent 数据流非常有帮助。Kotlin 的语法糖在简化代码的同时,也把一些底层机制给“藏”起来了,入门阶段先看明白底层怎么回事,再切 Kotlin,反而学得更扎实。
1.3 功能模块划分
屏幕录制器虽然看起来功能单一,但拆开来看至少要包含这么几个模块:权限管理模块(负责请求录屏授权和录音权限)、录屏服务模块(负责后台录制的主流程)、悬浮窗控制模块(负责开始、暂停、停止的控制交互)、视频管理模块(负责文件保存和录制后处理)。
划分清楚模块,开发效率会高很多。我最初做这个项目时,把所有逻辑都堆在 Activity 里,结果一按 Home 键,录制就中断了,因为 Activity 被系统回收后,录制线程也跟着没了。后来把录制迁移到前台服务,才彻底解决了后台录制被杀的问题。这个思路很重要,虽然写完功能主要代码在 500~800 行之间,但如果模块划分混乱,维护起来会十分痛苦。
2. 环境搭建与关键技术选型经验
2.1 Android Studio 环境与 SDK 版本
开发这个项目,环境要求不高。Android Studio 用官方最新稳定版就行,SDK 版本建议 minSdk 设为 21(Android 5.0),targetSdk 设为 33 或 34。为什么这么设?因为MediaProjection API 在 Android 5.0(API 21)才正式推出,这是硬底线,minSdk 低于这个版本直接没法跑。targetSdk 设得高一些,是为了适配新版系统对权限和后台执行的限制,尤其是 Android 10 及以后对后台启动 Activity 的限制,会影响录屏的授权流程。
有个非常容易踩的坑:Android 10(API 29)及以上,MediaProjection 要求必须配合前台服务使用,否则系统会直接抛出 SecurityException。我在 Android 12 的真机上测试时,如果不启动前台服务,授权成功后屏幕录制自动崩溃,就是这个原因。
2.2 核心 API 选型:MediaProjection + MediaRecorder
录制方案的 API 有两种选择:一是 MediaRecorder 直接结合 MediaProjection,这是最简单、最容易上手的方案;二是用 Surface 配合 MediaCodec + MediaMuxer 做自定义编码,这个方案更灵活,但代码量翻倍。作为初学项目,我用的是 MediaRecorder 路线。
参数设置上,有几个关键值要特别注意:
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setVideoSize(width, height); mediaRecorder.setVideoFrameRate(30); mediaRecorder.setVideoEncodingBitRate(4_000_000); mediaRecorder.setAudioEncodingBitRate(128_000); mediaRecorder.setAudioSamplingRate(44100);录制分辨率最好不要直接用屏幕物理分辨率。我实测过,用 2K 屏真机录屏时,如果按原始分辨率输出,码率要拉到 20Mbps 以上才不糊,文件体积巨大。比较明智的做法是手动缩放,比如 1080p 的屏幕缩到 720p 来录,体积小很多,清晰度也没差太多。缩放的原理就是计算屏幕宽高比后等比例缩小,代码在下一章会给出。
2.3 为什么要用前台服务
录屏本质是个长时任务,用户授权之后可能切去别的应用操作,录屏得继续在后台跑。Android 系统对后台任务管控很严,普通 Service 在 Android 8.0 之后一旦进程被系统判定为低优先级,就很容易被回收。解决办法是把它变成前台服务,同时显示一个常驻通知,让系统知道“这个任务正在被用户使用中”,这样进程的优先级就高了,被回收的概率大大降低。
这里也有个适配细节:从 Android 13(API 33)开始,使用前台服务必须声明对应的前台服务类型,录屏对应的类型是 mediaProjection。Manifest 里要这么写:
<service android:name=".RecordService" android:foregroundServiceType="mediaProjection" android:exported="false" />同时动态申请权限的时候,POST_NOTIFICATIONS 这个通知权限也别漏掉,否则前台服务启动时会直接拒绝显示通知,服务也起不来。
3. 完整源码实现流程解析
3.1 第一步:申请录屏授权
这个过程可以用一次 Intent 跳转来概括。先获取 MediaProjectionManager 系统服务,然后通过它创建屏幕捕获 Intent,跳转到系统授权页面,用户点击“允许”后,系统会把一个 resultCode 和 data 回传到 onActivityResult 里。这两个数据是后面创建 MediaProjection 实例的凭证,一定不能丢。
private void requestScreenCapture() { MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); Intent captureIntent = projectionManager.createScreenCaptureIntent(); startActivityForResult(captureIntent, REQUEST_CODE_CAPTURE_PERMISSION); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_CAPTURE_PERMISSION) { if (resultCode == RESULT_OK && data != null) { // 这里拿到的 resultCode 和 data 要传给服务 startRecordService(resultCode, data); } else { // 用户拒绝授权,给提示 Toast.makeText(this, "需要录屏授权才能开始录制", Toast.LENGTH_LONG).show(); } } }有一个很隐蔽的坑:授权用的 resultCode 不能自己随便定义,必须是系统返回的那个值。很多教程里会写“resultCode == -1”,但规范写法是直接用 Activity.RESULT_OK 去判断,这样代码语义更清晰,也不容易出问题。
3.2 第二步:在前台服务中创建 MediaProjection
拿到授权结果后,不要直接在 Activity 里创建 MediaProjection,最好是传给服务去初始化。原因是 Activity 有完整的生命周期,旋转屏幕或者被回收后,媒体投影对象会失效。而服务相对稳定,录制的核心逻辑放这里最合适。
public class RecordService extends Service { private MediaProjection mediaProjection; private MediaRecorder mediaRecorder; private VirtualDisplay virtualDisplay; private boolean isRecording = false; private int screenWidth; private int screenHeight; private int screenDensity; public void startRecording(int resultCode, Intent resultData) { MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); mediaProjection = projectionManager.getMediaProjection(resultCode, resultData); if (mediaProjection == null) { // 授权数据失效,需要重新发起授权 return; } initRecorder(); createVirtualDisplay(); } }创建 VirtualDisplay 时注意,要在 mediaProjection.createVirtualDisplay 方法里传一个 Surface 参数。这个 Surface 必须是从 MediaRecorder 的 Surface 接口里拿到的,不能自己 new 一个 SurfaceView 的 Surface 传进去,否则录出来是黑屏。这是我调试过程中印象最深的一个教训,网上很多帖子都卡在这一步。
private void createVirtualDisplay() { MediaRecorder recorder = mediaRecorder; if (recorder == null) { return; } Surface surface = recorder.getSurface(); virtualDisplay = mediaProjection.createVirtualDisplay( "ScreenRecorderDisplay", screenWidth, screenHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null); }3.3 第三步:完整配置 MediaRecorder
MediaRecorder 的配置顺序是有讲究的,顺序不能乱。先设视频源和音频源,再设输出格式,然后是编码器、分辨率、帧率、码率。配完之后必须先 prepare(),再调 start()。prepare 失败会导致崩溃,所以一定要用 try-catch 包住。
下面是我整理过的一段可以直接用的配置逻辑,注释写清楚了每个参数选型的原因:
private void initRecorder() { if (mediaRecorder != null) { mediaRecorder.release(); } mediaRecorder = new MediaRecorder(); // 视频源用 SURFACE,因为 MediaProjection 输出就是 Surface mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 音频源用 MIC,即手机麦克风 mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 输出格式用 MPEG_4,兼容性最好 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // H264 是通用级最高的编码,几乎什么播放器都能放 mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // AAC 音频编码,体积小音质好 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 输出分辨率,这里可以自行缩放 mediaRecorder.setVideoSize(screenWidth, screenHeight); mediaRecorder.setVideoFrameRate(30); // 视频码率 4Mbps 是一个比较均衡的值 mediaRecorder.setVideoEncodingBitRate(4 * 1024 * 1024); // 音频码率 128Kbps,采样率 44100Hz mediaRecorder.setAudioEncodingBitRate(128 * 1024); mediaRecorder.setAudioSamplingRate(44100); // 输出路径,注意 Android 10 之后分区存储的适配 videoFilePath = getExternalFilesDir(null) + "/screen_record_" + System.currentTimeMillis() + ".mp4"; mediaRecorder.setOutputFile(videoFilePath); try { mediaRecorder.prepare(); } catch (IOException e) { e.printStackTrace(); // prepare 失败需要重置 recorder } }关于输出路径多说一句,Android 10(API 29)之后不能直接往公共存储目录写文件了,一定要用 getExternalFilesDir() 这种应用专属目录,或者用 MediaStore 去申请公共目录写入。之前有同学用旧代码跑在 Android 11 上,录制一开始就崩,查了半天发现是文件权限的问题。
3.4 第四步:开始录制与暂停恢复
开始录制的代码顺序非常重要,必须严格按照MediaRecorder.start() → VirtualDisplay 创建的顺序来,这个顺序不能反。我的理解是,如果先创建 VirtualDisplay,屏幕上每一帧画面就会往 MediaRecorder 的 Surface 上发送,但这个时候 MediaRecorder 还没进入录制状态,画面数据进来了也录不上,就会产生一帧都录不到或者首帧黑屏的问题。
public void startRecording() { if (isRecording) { return; } isRecording = true; mediaRecorder.start(); createVirtualDisplay(); }暂停和恢复的实现更复杂一点。因为 MediaProjection 的 VirtualDisplay 不支持暂停,标准的做法是先 stop() 并 release() 掉 MediaRecorder,存下当前文件,然后再 new 一个新的 MediaRecorder 和新的 VirtualDisplay 继续录,最后把多个视频文件合并。这里不展开讲合并的细节,因为会引入 FFmpeg 之类的额外工具,但至少要清楚,简单项目用“暂停”不如直接用“停止并保存”来得干净。
3.5 第五步:停止录制与资源释放
停止录制的代码顺序和开始相反:先停止 VirtualDisplay,再停止 MediaRecorder,最后释放 MediaProjection。资源释放不彻底的话,下次录制会报 MediaRecorder 初始化失败,这个也是实战中非常高频的问题。
public void stopRecording() { if (!isRecording) { return; } isRecording = false; if (virtualDisplay != null) { virtualDisplay.release(); virtualDisplay = null; } if (mediaRecorder != null) { try { mediaRecorder.stop(); } catch (RuntimeException e) { // mediaRecorder 没有数据时 stop 会抛异常,需要捕获处理 e.printStackTrace(); } mediaRecorder.release(); mediaRecorder = null; } if (mediaProjection != null) { mediaProjection.stop(); mediaProjection = null; } }注意那个 RuntimeException 捕获,这是我从崩溃日志里总结出来的。如果你在 start() 之后很快就 stop(),MediaRecorder 可能还没有写入任何有效数据,此时 stop() 会直接抛 MediaRecorder 内部状态异常,导致应用崩溃。这种情况只需要删掉已生成的空文件即可,文件本身没有意义了。
4. 悬浮窗控制与录制体验优化
4.1 悬浮窗的两种实现方式
录屏的时候,用户肯定要去操作其他界面,不可能一直停在自己的 App 里,所以需要一个悬浮窗来控制录制状态。实现方案有两种,一种是 TYPE_APPLICATION_OVERLAY 悬浮窗,所有 Android 版本都支持,但要动态申请“显示在其他应用上层”的权限;另一种是画中画模式,Android 8.0 以后系统支持,但控制和交互不如悬浮窗灵活。大多数录屏 App 用的是第一种,我们这里也讲第一种。
4.2 权限申请与悬浮窗创建
悬浮窗权限的申请不是弹窗式的,而是跳转到系统设置页让用户手动打开。所以在 MainActivity 里要先判断 Settings.canDrawOverlays(),没权限就跳设置页。这部分代码虽然简单,但没写完的话,悬浮窗怎么都显示不出来。
if (!Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } else { // 有权限,直接显示悬浮窗 showFloatWindow(); }创建悬浮窗用 WindowManager,要设定宽高类型,最关键的是 targetSdk 在 26 及以上时,悬浮窗类型必须用 TYPE_APPLICATION_OVERLAY,用旧的 TYPE_PHONE 会直接报错。悬浮窗上放一个 Button,点击开始录制,再点一下变成停止,这就算一个最基础的控制面板了。
4.3 录屏通知栏常驻提示
聊到体验,录屏时通知栏常驻提醒是必不可少的。一方面是前台服务本身要求必须有通知,另一方面也是《个人信息安全规范》的要求,让用户知道当前有录制行为在发生。通知内容我会实时更新录制的时长或者状态,这样用户心里有数。具体实现是通过 NotificationManager.notify() 来更新通知内容,比较基础。
4.4 录音开关:内录与麦克风
很多录屏 App 支持录制系统内部声音,但 MediaRecorder 的 MIC 音源只能录制麦克风输入的声音。要实现内录,Android 10 及以上可以通过 AudioPlaybackCaptureConfiguration 配合 MediaCodec 实现,这是相对高级的用法。初学阶段用 MIC 就行,但也想提前提醒大家:真机测试时注意关掉扬声器,否则录出来的声音里全是回声。
5. 常见问题详解与排查技巧
5.1 黑屏问题排查
录出来是黑屏,这是屏幕录制器最最经典的问题。排查路径我建议按下面这个顺序来:
- 确认 MediaProjection 是否创建成功,getMediaProjection() 返回 null 说明授权数据失效,要重新授权。
- 确认 VirtualDisplay 的 Surface 是否为 null,Surface 为 null 的情况通常是 MediaRecorder 没有 prepare 成功。
- 确认 start 顺序正确,先 MediaRecorder.start() 再 createVirtualDisplay()。
- 确认输出文件没有被损坏,停止时 stop() 有没有抛异常。
我遇到过一种很诡异的情况:在 Android 模拟器上录制一切正常,换到真机上就黑屏。后来发现是模拟器的 GPU 渲染方式跟真机不一样,真机必须设置 VirtualDisplay 的 density,不设置或设置不对,就会出现画面偏移或黑屏。所以真机调试时,screenDensity 一定要用 resources 里的 densityDpi,不要写死。
5.2 录制一打开就崩溃
崩溃问题大多集中在 Android 版本适配。Android 10 以上没启动前台服务,会导致授权成功就崩;Android 13 以上没声明前台服务类型,会导致服务启动失败;Android 14 以上如果是 targetSdk 34 的项目,MediaProjection 的授权回调形式还发生了变化。这就体现出用真机测试的重要性,模拟器上很多版本限制是不生效的。
5.3 录制没有声音
没有声音的排查顺序是:录音权限是否授予、音频源是否设置、录制时是否插了耳机、系统录音被其他应用占用。我遇到过一个比较极端的案例:某品牌手机在录制前打开了“应用内音量”控制,结果录出来的视频前 10 秒没有声音,排查半天发现是系统对录音启动时间做了延迟处理。这种情况没有特别好的通用解决方案,能做的是在开始录制前等待 200 毫秒,让录音源稳定后再启动 MediaRecorder。
5.4 文件体积暴涨
同一段视频,体积翻倍的情况很常见,原因基本是码率设置过高或者分辨率没有缩放。我的建议是:普通教学视频用 4Mbps 的 720p 完全够用,2K 屏幕可以把分辨率缩到 1080p,码率设为 8Mbps,这样录出来的文件既清楚又不至于大到没法分享。另外视频帧率设到 30fps 就够了,60fps 对屏幕录制来说意义不大,还会明显增加体积。
5.5 常见问题速查表
| 问题表现 | 可能原因 | 解决方案 |
|---|---|---|
| 授权成功后立即崩溃 | Android 10+ 没有前台服务配合 | 将录屏逻辑迁移到前台服务 |
| 录制视频黑屏 | VirtualDisplay 的 Surface 来源错误 | 必须用 MediaRecorder.getSurface(),不能自定义 Surface |
| 停止录制时崩溃 | stop() 过早调用,内部状态异常 | 捕获 RuntimeException,删除空文件 |
| 无法创建前台服务 | 未声明 foregroundServiceType | Manifest 中添加 mediaProjection 类型 |
| 录制文件保存失败 | 分区存储适配问题 | 使用 getExternalFilesDir() 或 MediaStore |
| 第二次录制失败 | 上次资源未释放干净 | release 所有 MediaProjection/VirtualDisplay/MediaRecorder |
6. 进阶扩展方向与个人实操体会
功能跑通之后,下一步怎么扩展,我根据自己的经验给几条路线参考。
第一,增加屏幕区域选择和摄像头 PiP。通过 MediaProjection 创建 VirtualDisplay 时可以指定裁剪区域,这里的核心是计算正确的 sourceRect 和 destRect 换算。加了摄像头画中画后,就能做解说录屏,很多网课录屏工具的核心功能就是这个。
第二,增加录制文件管理页面。录制结束在通知里弹出保存成功提示,点击跳转到视频列表页,列表页展示时长、文件大小、分辨率,支持点击播放和分享。这个功能能让你把 RecyclerView、文件扫描、视频缩略图加载这几个常见技能点都过一遍。
第三,引入 FFmpeg 做视频后处理。比如合并多个分段文件、压缩视频、提取音频,甚至叠加时间戳和自定义水印。FFmpeg 在 Android 端的集成方式有 MobileFFmpeg 和自编译两种,自编译会比较折腾,但对理解交叉编译很有帮助。这部分是很多公司招聘时的加分项。
第四,换用 MediaCodec + MediaMuxer 自定义编码方案。MediaRecorder 是个黑盒,灵活性有限,如果你想实现动态切换分辨率、自定义码率曲线、实时帧处理这些高级功能,就必须走 MediaCodec。这套方案的代码量会多出两倍不止,但走通一遍之后,你对整个音视频数据流的理解会上一个台阶。
第五,利用 CameraX 实现摄像头与屏幕源混流。这属于创作工具方向,比如录制手机屏幕同时把小窗口的摄像头画面叠加到指定位置。底层原理是用 SurfaceView 预览摄像头画面,再通过 OpenGL 做离屏渲染将两层画面合成为一张纹理,最终输出到 MediaCodec 编码输入。这是一个很有挑战但也非常有趣的进阶方向。
最后说说我心里比较真实的体会。屏幕录制器这个项目非常适合作为 Android 学习的中期检验项目,原因在于它把权限管理、服务生命周期、系统底层 API 调用、音视频编码、UI 交互这几大块知识全部串起来了。做完一遍,你对 Android 应用如何使用系统能力会有很直观的感知,这种感知是刷多少道八股文都换不来的。
还有一个实际一点的建议:如果做毕设,不要只做“录屏”这一个纯功能项,可以加上“录屏+视频编辑”组合,或者做成“在线课堂录制作业提交工具”,有具体的业务场景作依托,答辩时也更好讲。而在动手写代码之前,强烈建议先把 MediaProjection 官方文档从头到尾过一遍,同时找一台 Android 10 以上的真机作为主要测试设备,因为版本适配的坑在模拟器上基本试不出来。
单说这个项目本身,里面反复体现出来的一个原则是“资源管理的顺序决定成败”——start 的顺序、stop 的顺序、release 的顺序,错了就是黑屏、崩溃、二次录制失败。这说起来是老生常谈,但只有真正调试过、崩溃过、对着 logcat 一行一行排查过之后,你才会真正理解为什么顺序这么重要。希望这篇文章能帮你在做录屏器的路上少走这一圈弯路。