Android屏幕录制器开发实战:MediaProjection原理与源码解析
发布时间:2026/9/10 16:13:12 锦皓数字建站

做个屏幕录制器说难不难但真要做得顺手、录出来不出问题还是有一堆细节要处理的。尤其是把它当成课程设计、毕业设计或者练手项目很多人都卡在“源码能跑但自己写就崩”这个坎上。这篇文章我就拿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 设为 21Android 5.0targetSdk 设为 33 或 34。为什么这么设因为MediaProjection API 在 Android 5.0API 21才正式推出这是硬底线minSdk 低于这个版本直接没法跑。targetSdk 设得高一些是为了适配新版系统对权限和后台执行的限制尤其是 Android 10 及以后对后台启动 Activity 的限制会影响录屏的授权流程。有个非常容易踩的坑Android 10API 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 13API 33开始使用前台服务必须声明对应的前台服务类型录屏对应的类型是 mediaProjection。Manifest 里要这么写service android:name.RecordService android:foregroundServiceTypemediaProjection android:exportedfalse /同时动态申请权限的时候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 第三步完整配置 MediaRecorderMediaRecorder 的配置顺序是有讲究的顺序不能乱。先设视频源和音频源再设输出格式然后是编码器、分辨率、帧率、码率。配完之后必须先 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 10API 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 是否为 nullSurface 为 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删除空文件无法创建前台服务未声明 foregroundServiceTypeManifest 中添加 mediaProjection 类型录制文件保存失败分区存储适配问题使用 getExternalFilesDir() 或 MediaStore第二次录制失败上次资源未释放干净release 所有 MediaProjection/VirtualDisplay/MediaRecorder6. 进阶扩展方向与个人实操体会功能跑通之后下一步怎么扩展我根据自己的经验给几条路线参考。第一增加屏幕区域选择和摄像头 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 一行一行排查过之后你才会真正理解为什么顺序这么重要。希望这篇文章能帮你在做录屏器的路上少走这一圈弯路。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。