FEATURED · 精选文章

Android截屏录屏避坑指南:MediaProjection从入门到Android 14适配

发布时间 / 2026/9/9 12:38:18
来源 / 创域科博编辑部
栏目 / 资讯中心
Android截屏录屏避坑指南:MediaProjection从入门到Android 14适配 简介面向Android开发者的MediaProjection截屏与录屏适配方案针对Android 10Q及以上系统常见的SecurityException异常给出基于前台服务FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION的正确配置与实际可运行Demo适用于targetSdk 32及以上的权限适配场景。包内含42个文件以Kotlin源码、XML布局/清单、Gradle构建脚本为主辅以webp示意截图与工程配置文件压缩包仅120KB结构紧凑、便于快速导入分析。目前已有4007人学习下载适合正被系统弹窗或崩溃问题困扰或希望快速落地系统级录屏功能的中高级Android工程师。通过该Demo可掌握前台服务类型声明、动态权限申请与MediaProjection回调解耦思路直接绕开Android版本差异带来的兼容性坑。 做Android开发这几年被问得最多的一个需求就是截屏和录屏。无论是做社交分享、客服反馈、在线教育还是单纯做一个系统工具类App屏幕捕获几乎都是标配功能。而只要提到Android端的截屏录屏MediaProjection就是绕不开的那套官方API。它从Android 5.0API 21被引入一直沿用到现在期间踩过的坑、适配过的问题真的可以写一本书。这篇就把我沉淀下来的方案和教训一次性理清楚给正准备接入MediaProjection或者正在调崩溃的朋友做个参考保证你看完能少走不少弯路。MediaProjection这套东西说简单也简单说复杂也复杂。简单在于它拿到授权之后就是把屏幕内容渲染到一个Surface上复杂在于版本差异、权限限制、生命周期管理、资源释放顺序任何一个环节没处理好线上就是一堆crash。所以这篇我从方案选型开始讲一直讲到Android 14的适配细节全程按我的实操经验来。1. 方案选型为什么MediaProjection是截屏录屏的最优解1.1 MediaProjection是什么能做什么MediaProjection是Android官方提供的屏幕捕获API它允许应用在用户明确授权的情况下获取当前屏幕的实时画面帧。你拿到这些帧数据之后可以拿去编码成视频文件也可以转成Bitmap保存为图片甚至可以推流到远端做投屏或者远程控制。它的核心能力就是一句话把一块“虚拟屏幕”的内容持续渲染到指定的Surface上。实际项目中我见过用它做截屏工具的做游戏录屏的做实时屏幕共享的还有做用户行为录制的比如客服系统回放用户操作路径。从功能覆盖面上看只要是跟“屏幕画面”打交道MediaProjection基本都能胜任。而且它不需要root权限对普通应用来说非常友好。1.2 几类常见截屏/录屏方案的横向对比在入坑MediaProjection之前我其实还调研过其他方案这里直接给大家做个对比省得你们再重复踩一轮。方案是否需要root能否录屏画面完整性接入手感MediaProjection否可以完整捕获所有内容中等主要靠官方APIroot下的screencap/screenrecord是可以完整依赖系统版本兼容性差无障碍服务AccessibilityService否不能只能读控件节点拿不到像素不适合做画面捕获厂商私有API反射部分需要部分可以不稳定一行代码都嫌多结论很明确普通App想要稳定地截屏、录屏MediaProjection是唯一兼顾兼容性和功能完整性的方案。root方案在用户设备上几乎没有覆盖率无障碍方案拿不到真实画面厂商私有API更是随时可能被封掉。所以不用纠结直接用MediaProjection。2. 核心流程拆解从授权弹窗到MediaProjection实例2.1 三行代码发起授权为什么结果不能缓存先看最核心的一段逻辑。要使用MediaProjection第一步永远是请求用户授权这个授权弹窗由系统弹出用户必须在弹窗上点击“允许”或“开始录制”之类的按钮你的应用才能拿到屏幕内容。代码大概是这样MediaProjectionManager mpm (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); Intent captureIntent mpm.createScreenCaptureIntent(); startActivityForResult(captureIntent, REQUEST_CODE_CAPTURE);然后在Activity的onActivityResult里拿到授权结果Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode REQUEST_CODE_CAPTURE resultCode RESULT_OK) { MediaProjectionManager mpm (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); MediaProjection mediaProjection mpm.getMediaProjection(resultCode, data); // 到这里才算真正拿到了屏幕捕获的“钥匙” } }这里有个非常关键的点resultCode和data不能缓存、不能跨进程保存。因为系统为了安全强制要求“每次会话都要重新授权”。你就算把resultCode和data存到本地下次App启动后再拿出来用系统也不会认账。Android 14之后这个限制更是被提到了明面上一旦发现你用旧数据创建MediaProjection直接给你抛异常。所以在设计架构的时候务必把所有跟MediaProjection相关的状态都做成内存级、会话级不要有任何持久化缓存的想法。2.2 VirtualDisplay把屏幕“投影”到自定义Surface拿到MediaProjection对象之后真正干活的其实是VirtualDisplay。可以把它理解成一面镜子系统会把真实屏幕的内容持续映射到这面镜子上而镜子的“落点”由你指定。这个落点就是一个Surface不同类型的Surface决定了你是在截屏还是在录屏。VirtualDisplay virtualDisplay mediaProjection.createVirtualDisplay( ScreenCaptureDisplay, width, height, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, // 这里传入ImageReader.getSurface()或MediaRecorder.getSurface() null, null );我在实际项目里最常犯的错误就是忽略这个Surface的类型选择。如果传的是ImageReader的Surface那VirtualDisplay每次刷新都会回调一张图片给你如果传的是MediaRecorder.getSurface()那画面会直接流进编码器变成视频帧。搞清楚这一点后面的截屏和录屏逻辑就都好理解了。另外注意createVirtualDisplay的width和height最好是屏幕的实际像素不要用布局里的宽高dp。否则可能出现区域截取或画面被拉伸的问题这个在后面适配部分会细说。3. 静态截屏实战ImageReader取帧的正确姿势3.1 创建VirtualDisplay与ImageReader静态截屏的思路很简单用ImageReader接收一帧数据转成Bitmap保存。代码骨架如下ImageReader imageReader ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2); imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { Override public void onImageAvailable(ImageReader reader) { Image image reader.acquireLatestImage(); if (image ! null) { // 这里处理Bitmap image.close(); // 千万不能漏 } } }, backgroundHandler); mediaProjection.createVirtualDisplay(ScreenCapture, width, height, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.getSurface(), null, null);这里有几个容易踩的坑我一个个说。第一ImageReader的像素格式必须是RGBA_8888不要用ImageFormat.PRIVATE。PRIVATE格式虽然性能更好但它拿不到可以直接转Bitmap的像素数据只适合丢给编码器用。你要想从Image对象里读出像素来就必须用RGBA_8888。第二maxImages这个参数建议设置成2。设成1的时候如果一帧还没处理完系统下一帧就来了会直接把旧的丢弃导致画面丢帧或截出来是黑的。设成3以上又没必要白白增加内存占用。第三拿到Image之后务必调用close()。我之前第一次写截屏功能的时候忘了关Image结果连续截十几张图之后内存直接飙升到几百兆最后App被系统杀掉。那个错误日志我到现在还记得是OutOfMemoryError排查了好久才发现是这个坑。3.2 拿到Bitmap后如何安全保存含Android 10适配从Image转Bitmap一般先拿到Image.Plane再复制像素Image.Plane[] planes image.getPlanes(); ByteBuffer buffer planes[0].getBuffer(); int pixelStride planes[0].getPixelStride(); int rowStride planes[0].getRowStride(); int rowPadding rowStride - pixelStride * width; Bitmap bitmap Bitmap.createBitmap(width rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(buffer); bitmap Bitmap.createBitmap(bitmap, 0, 0, width, height);保存的时候Android 10之前可以直接写公共存储目录比如sdcard/Pictures/但如果你targetSdk升到29及以上就必须用MediaStore或者应用专属目录。我自己的习惯是截屏类App优先用MediaStore写入系统图库这样用户能在相册里直接看到ContentValues values new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, screenshot_ System.currentTimeMillis() .png); values.put(MediaStore.Images.Media.MIME_TYPE, image/png); values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); Uri uri getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); try (OutputStream os getContentResolver().openOutputStream(uri)) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); }这种方式的好处是不用申请存储权限也能让文件出现在系统相册里省去一堆权限适配的麻烦。3.3 方向与尺寸适配别再把宽高写反截屏最让我头疼的其实是宽高问题。很多手机在竖屏状态下逻辑宽高是1080x2400但如果用户横屏玩手机这时候再去取getResources().getDisplayMetrics().widthPixels拿到的可能是2400x1080而实际上屏幕本身并没有变。直接拿这个宽高创建VirtualDisplay截出来的图很可能是旋转了90度的或者是被裁掉一部分的。我的做法是用Display.getRealSize或WindowManager.getCurrentWindowMetrics来获取当前窗口真正的实际尺寸并且根据Configuration的orientation做一次判断手动调整宽高顺序。虽然代码不复杂但漏掉这一步线上反馈“截屏方向不对”的问题就会一直冒出来。4. 录屏实战MediaRecorder VirtualDisplay完整流程4.1 MediaRecorder参数配置思路录屏相比截屏多了一个编码环节。最简单的方案是用MediaRecorder因为它在内部封装了编码器你不需要自己跟MediaCodec打交道。配置参数的时候我习惯这样写MediaRecorder mediaRecorder new 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(8 * 1024 * 1024); mediaRecorder.setAudioEncodingBitRate(128 * 1024); mediaRecorder.setAudioSamplingRate(44100); mediaRecorder.setOutputFile(filePath); mediaRecorder.prepare();这里有个很重要的顺序必须先prepare()再拿getSurface()。因为Surface是在prepare之后才真正创建的你提前取只会拿到null。码率这个参数我建议不要盲目设置太高。1080p的视频8Mbps在大多数设备上已经很清晰了再往高调编码压力大手机发热严重反而容易掉帧。如果录制的场景画面变化不大比如在线文档甚至可以把码率降到4Mbps文件体积能小一半。4.2 启动和停止的严格顺序MediaRecorder的启动和停止顺序错了就会崩溃这个我太有发言权了。第一次做录屏的时候我写了mediaRecorder.start()之后马上创建VirtualDisplay结果在某些机型上直接抛IllegalStateException。后来查了半天才发现start()之后系统需要一小段时间准备Surface正确的姿势是先创建VirtualDisplay再调用start()VirtualDisplay virtualDisplay mediaProjection.createVirtualDisplay( ScreenRecorder, width, height, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mediaRecorder.getSurface(), null, null ); mediaRecorder.start();停止的时候顺序反过来先stop MediaRecorder再release VirtualDisplay最后stop MediaProjection。如果先release了VirtualDisplayMediaRecorder会因为Surface被销毁而报错如果先stop了MediaProjectionMediaRecorder会拿不到新的帧数据同样可能崩。我最后的经验是写一个固定顺序的方法每次录屏结束都走同一个流程基本不会再踩这个坑。4.3 声音采集外录与内录的选择录屏有没有声音是很多产品需求的痛点。MediaRecorder的AudioSource.MIC是采集麦克风也就是环境音。如果你走到会议室里演示周围人说话都会被录进去这通常不是我们想要的。从Android 10开始系统提供了AudioPlaybackCaptureConfiguration可以做到“内录”也就是只捕获App内部播放的声音AudioPlaybackCaptureConfiguration config new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) .build(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); // 需要配合内录配置不过要提醒一句内录方案在不同厂商ROM上的兼容性不是特别稳定有些手机会因为硬件抽象层不同而失败。所以我在实际项目中通常做成可配置项默认用MIC顺手录环境音同时开放“系统播放声音”的选项。这样做用户选择更灵活后台也好排查问题。5. 版本适配实战从Android 5.0到Android 145.1 Android 10分区存储与后台限制Android 10API 29对MediaProjection相关的影响主要有两块。第一是分区存储应用不能再随意往公共目录写文件必须用MediaStore或者Context.getExternalFilesDir()。第二是后台启动Activity的限制你在录屏过程中想弹一个提示框、跳转一个页面如果App在后台startActivity会被系统拦下来。这两点对录屏场景的影响非常直接。我的解决方案是引入前台服务把录屏过程放在一个带mediaProjection类型的前台服务里。这样一来即使App退到后台录屏也不会被系统杀掉而且用户能看到常驻通知知道“正在录屏”对隐私也更友好。Android 14之后前台服务类型的要求更严格下面展开说。5.2 Android 14MediaProjection回调与前台服务类型Android 14API 34可能是MediaProjection适配里最绕不开的一道坎。它有两个硬性变化不处理就是crash。第一个变化是必须注册MediaProjection.Callback。在Android 14上如果你创建了MediaProjection之后不调用registerCallback()当用户从状态栏撤销屏幕捕获授权时系统会直接让你的进程崩溃。官方注释原话大意是为了确保应用能及时释放资源你必须注册一个callback。我最开始没注意升级targetSdk之后一测试发现用户点了一下“停止”App就闪退了日志里赫然写着需要在createVirtualDisplay前注册callback。mediaProjection.registerCallback(new MediaProjection.Callback() { Override public void onStop() { // 用户撤销授权这里必须释放VirtualDisplay、停止MediaRecorder } }, handler);第二个变化是每次会话都必须重新授权。前面也强调过resultCode和data不能缓存。Android 14把“必须重新走授权弹窗”变成了强制逻辑你要是尝试复用旧的MediaProjection实例系统会直接抛SecurityException。所以App内部要设计好状态一旦用户完成了截屏/录屏MediaProjection实例就该被销毁下次再发起时重新走一遍授权流程。另外前台服务在Android 14上必须声明mediaProjection类型并且要在startForeground()时同时传入FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION。而且系统规定从发起授权到启动前台服务有一个时间窗口限制如果太久不调用startForeground服务会被系统判为违规。我的做法是把授权结果回调、服务启动、VirtualDisplay创建放在同一条执行链路上尽量缩短间隔。5.3 折叠屏、多窗口与分辨率变化从Android 12开始可折叠设备和多窗口形态越来越普及这对MediaProjection的适配也提出了新要求。如果你在折叠屏展开状态下开始录屏然后用户突然折叠了手机VirtualDisplay里记录的宽高并不会自动更新结果就是录出来的画面比例跟实际屏幕不一致。我在做这块适配时会监听DisplayListener或者Configuration变化在屏幕尺寸发生改变时重新创建VirtualDisplay。注意这里不能直接原地改参数必须先release掉旧的VirtualDisplay再用新的宽高创建。多窗口模式也是同理如果你的App支持分屏那么MediaProjection捕获到的是整个屏幕还是你App所在的窗口区域默认情况下是整个屏幕这样用户会觉得“我明明只想录这个窗口为什么状态栏也被录进去了”。如果要精确到窗口需要结合自定义Surface的裁剪逻辑复杂度会高一个量级一般非必要不建议做。6. 高频问题排查与避坑手册6.1 黑屏、崩溃、无声问题速查表把我在群里和社区里被问得最多的问题整理成一张表你们遇到类似情况直接对照排查。现象可能原因解决方案授权后立刻黑屏ImageReader格式用成了PRIVATE换成PixelFormat.RGBA_8888截屏画面方向不对横竖屏切换时宽高没适配用getRealSize或WindowManager获取当前实际尺寸MediaRecorder stop崩溃释放顺序不对固定顺序recorder.stop - display.release - projection.stop持续截屏内存暴涨Image没调用close在onImageAvailable里务必image.close()Android 14上撤销授权崩溃没有注册Callback创建MediaProjection后立即registerCallback保存图片找不到文件分区存储用MediaStore或getExternalFilesDir录屏没有声音没有RECORD_AUDIO权限动态申请麦克风权限且audioSource选MIC录屏卡顿掉帧码率或帧率设置太高1080p建议8Mbps30fps再高没意义二次截屏授权失败缓存了旧resultCode/data每次重新走startActivityForResult6.2 资源释放顺序我踩过最深的坑关于资源释放值得单独说一节。MediaProjection、VirtualDisplay、ImageReader、MediaRecorder这四个对象的生命周期是嵌套的。我自己总结了一个顺口溜先停录制再放镜像最后收投影。具体来说就是先停掉MediaRecorder或者释放ImageReader停止取帧然后调用VirtualDisplay.release()销毁镜像最后再调用MediaProjection.stop()关闭投影仪。顺序反了轻则IllegalStateException重则直接native crash。而且这些释放操作最好放在同一个线程里执行避免并发释放导致未知状态。6.3 性能优化如何长时间录屏不发热录屏是个吃性能的活尤其是长时间录屏比如录网课、录游戏攻略发热和卡顿几乎是必然的。我的优化思路有三条。第一降低帧率静态内容多的场景切到15fps动态场景30fps足够60fps除非特殊需求否则纯属自己为难自己。第二控制码率上面也提过8Mbps是甜点位。第三减少UI刷新录屏过程中尽量不要启动转场动画动画会带来大量帧内容变化编码器压力陡增。另外如果项目允许可以考虑用MediaCodec替代MediaRecorder做自定义编码流程虽然代码量翻倍但可以在编码前做降分辨率、抽帧等预处理性能上有质的提升。这个属于进阶玩法等你们把MediaRecorder方案跑通了再深入研究也不迟。我的个人体会是MediaProjection这套API本身并不难难在各种边界场景的系统差异。只要把握住“每次会话必须重新授权”“VirtualDisplay只是把屏幕镜像到Surface”“资源释放顺序固定不变”这三个核心点大部分问题都能迎刃而解。最后再分享一个小技巧真机调试时多准备几台不同Android版本和厂商ROM的设备特别是国产ROM它们对后台、权限的限制往往比原生系统更激进能帮你提前暴露很多线上才会出现的问题。希望这篇能帮你们少踩几个坑。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻