Android屏幕录制与截屏开发实战:从MediaProjection到性能优化
1. 项目概述与核心价值最近在做一个需要集成屏幕录制和截屏功能的应用发现网上资料虽然多但要么是过时的API要么只讲个皮毛真正把权限、兼容性、性能优化和用户体验讲透的少之又少。踩了一堆坑之后我决定把从Android 5.0到Android 14甚至预览版的完整实现路径、避坑指南和性能调优心得整理出来。这不仅仅是调用几个API那么简单它涉及到MediaProjection的权限流程管理、ImageReader的帧捕获效率、不同版本系统的兼容性策略以及如何平衡功能与功耗。无论你是想做一个内置录屏的工具类App还是需要在教育、远程协助或游戏直播类应用中嵌入此功能这篇从实战中总结的指南都能帮你绕过我走过的弯路直接构建一个稳定、高效且用户友好的解决方案。2. 整体方案设计与技术选型实现屏幕捕获Android提供了几条主要的技术路径选择哪一条取决于你的目标API级别、功能需求以及对性能和兼容性的要求。2.1 屏幕录制方案对比MediaProjection vs. 其他MediaProjection API (Android 5.0)这是目前官方推荐且功能最强大的方案。它的核心原理是获取一个代表当前屏幕内容的MediaProjection对象然后通过VirtualDisplay将屏幕画面投射到一个虚拟显示层再使用MediaRecorder或ImageReader从这个虚拟显示层获取数据。其最大优势在于可以录制系统音频需额外权限和应用内部音频并且录制过程对用户完全可见会有持续的通知提醒符合系统安全规范。注意从Android 10 (API 29) 开始MediaProjection需要在前台服务中启动并且该服务必须设置为foregroundServiceTypemediaProjection否则录制会被系统终止。adb shell screenrecord这是用于开发调试的命令行工具通过adb执行。它不适用于普通应用分发因为需要adb权限普通用户无法使用。但其实现原理底层也是MediaProjection和参数如码率、分辨率可以作为我们调试MediaRecorder参数的参考。第三方库与Root方案一些第三方库封装了MediaProjection提供了更便捷的API。而Root方案则通过获取系统最高权限直接访问帧缓冲区/dev/graphics/fb0这种方式兼容性极差破坏系统安全且绝大多数用户设备不具备Root条件强烈不推荐用于商业应用。结论对于需要上架应用商店、面向广大Android用户的功能MediaProjectionMediaRecorder/ImageReader是唯一可行且合规的技术选型。2.2 截屏方案对比MediaProjection vs. 反射取图MediaProjection ImageReader (Android 5.0)同样利用MediaProjection创建VirtualDisplay但关联一个ImageReader来捕获单帧图像。这是最灵活、可控性最高的方案可以获取原始的Image对象进而转换为Bitmap、保存为文件或进行图像处理。它支持全屏截取包括状态栏和导航栏。反射调用SurfaceControl.screenshot(需要系统权限)这是一个隐藏的API功能强大效率极高。但正因如此它需要应用声明READ_FRAME_BUFFER权限而该权限只对系统应用android:sharedUserIdandroid.uid.system或签名与平台证书一致的应用开放。普通第三方应用无法使用调用会直接抛出SecurityException。adb shell screencap同screenrecord仅限调试。结论对于第三方应用实现静默、一键截屏无需用户每次确认几乎是不可能的这是Android系统出于安全考虑的设计。我们能做的是通过MediaProjection获取用户一次授权后在授权会话有效期内进行多次截屏。因此MediaProjectionImageReader同样是截屏功能的最优解。2.3 项目架构设计基于以上分析我们的项目将采用统一的MediaProjection核心根据功能分支选择不同的数据消费端。权限与生命周期管理模块统一处理MediaProjectionManager的权限申请、MediaProjection对象的获取与缓存、以及关联的前台服务管理。屏幕录制子模块绑定MediaRecorder负责参数配置、文件输出、状态回调。屏幕截屏子模块绑定ImageReader负责单帧捕获、Bitmap转换与保存。UI与控制层提供简单的界面用于触发和控制录制/截屏流程。这种设计保证了代码复用也使得录制和截屏功能可以独立或同时工作。3. 核心实现细节与关键代码解析接下来我们深入到每个核心环节看看代码具体怎么写以及为什么要这么写。3.1 权限申请与MediaProjection获取这是整个流程的起点也是最容易出错的地方。// 1. 获取MediaProjectionManager val manager getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager // 2. 发起屏幕捕获意图 val captureIntent manager.createScreenCaptureIntent() startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE)在onActivityResult中处理用户授权结果override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode REQUEST_CODE_SCREEN_CAPTURE) { if (resultCode Activity.RESULT_OK data ! null) { // 3. 获取MediaProjection对象 mediaProjection manager.getMediaProjection(resultCode, data) // 重要立即启动一个前台服务并将mediaProjection传递给它 startForegroundServiceAndBind(mediaProjection) } else { // 用户拒绝授权处理相应逻辑 Toast.makeText(this, 屏幕捕获权限被拒绝, Toast.LENGTH_SHORT).show() } } }实操心得MediaProjection对象必须在授权后的短时间内使用并且其生命周期需要妥善管理。最佳实践是在onActivityResult中获取到对象后立即将其传递给一个独立的前台服务。这个服务将持有MediaProjection并负责后续的VirtualDisplay创建和资源释放。这样可以防止因Activity被销毁而导致录制中断。3.2 创建并管理前台服务从Android 10开始屏幕捕获必须运行在前台服务中。class ScreenCaptureService : Service() { private lateinit var mediaProjection: MediaProjection private var virtualDisplay: VirtualDisplay? null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 从Intent中获取传递过来的MediaProjection对象需序列化处理实际可用Token传递 // 创建通知并调用startForeground val notification createNotification() startForeground(NOTIFICATION_ID, notification) // 开始配置VirtualDisplay和MediaRecorder/ImageReader prepareCapture() return START_STICKY } private fun createNotification(): Notification { // 必须设置channelId (Android 8.0) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val channel NotificationChannel( CHANNEL_ID, 屏幕录制, NotificationManager.IMPORTANCE_LOW ).apply { description 正在录制屏幕内容 } getSystemService(NotificationManager::class.java).createNotificationChannel(channel) } return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(屏幕录制中) .setContentText(正在捕获屏幕内容) .setSmallIcon(R.drawable.ic_record) .setOngoing(true) // 持续通知 .build() } override fun onDestroy() { // 务必释放资源 virtualDisplay?.release() mediaProjection.stop() // 停止投影 super.onDestroy() } }在AndroidManifest.xml中声明服务并指定foregroundServiceTypeservice android:name.ScreenCaptureService android:enabledtrue android:exportedfalse android:foregroundServiceTypemediaProjection /3.3 实现屏幕录制功能在服务中我们使用MediaRecorder来编码和保存视频。步骤1配置MediaRecorder参数配置直接影响视频质量和文件大小。private fun setupMediaRecorder(filePath: String): MediaRecorder { return MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) // 录制麦克风声音 // 如果想录制系统内部音频在Android 10需要额外权限和不同AudioSource setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(44100) // 标准采样率 setAudioEncodingBitRate(128000) // 128kbps setVideoEncoder(MediaRecorder.VideoEncoder.H264) setVideoEncodingBitRate(5 * 1000 * 1000) // 5 Mbps可根据分辨率调整 setVideoFrameRate(30) // 30 fps setVideoSize(displayWidth, displayHeight) // 通常设置为屏幕分辨率 setOutputFile(filePath) prepare() // 这个调用可能耗时建议在子线程进行 } }步骤2创建VirtualDisplay并关联MediaRecorderprivate fun startRecording() { val mediaRecorder setupMediaRecorder(getOutputFilePath()) val surface mediaRecorder.surface virtualDisplay mediaProjection.createVirtualDisplay( ScreenRecorder, // 显示名称 displayWidth, displayHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, // 自动镜像 surface, // 将画面输出到MediaRecorder的Surface null, // 无回调 null // 无Handler ) mediaRecorder.start() }注意事项displayWidth,displayHeight,screenDensity需要动态获取。建议使用DisplayMetrics获取真实屏幕参数但录制高分辨率视频如4K对性能要求极高。一个常见的折衷方案是录制720P或1080P的视频可以通过在createVirtualDisplay时设置较小的宽高来实现“降采样”能显著降低CPU和内存占用。3.4 实现屏幕截屏功能截屏功能使用ImageReader作为VirtualDisplay的输出目标。步骤1配置ImageReader// 计算合适的ImageReader大小避免内存浪费 val maxImages 2 // 双缓冲通常足够 val imageReader ImageReader.newInstance( captureWidth, captureHeight, PixelFormat.RGBA_8888, // 常用格式便于转换为Bitmap maxImages ) // 设置监听器当有新帧可用时触发 imageReader.setOnImageAvailableListener({ reader - // 在这里获取并处理图像 acquireLatestImage(reader) }, handler) // 指定一个Handler决定在哪个线程回调步骤2创建VirtualDisplay并关联ImageReaderprivate fun startCapture() { virtualDisplay mediaProjection.createVirtualDisplay( ScreenCapture, captureWidth, captureHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.surface, // 关键输出到ImageReader的Surface null, null ) }步骤3从ImageReader中获取并处理图像private fun acquireLatestImage(reader: ImageReader) { var image: Image? null try { image reader.acquireLatestImage() // 获取最新的图像丢弃旧的 if (image ! null) { val planes image.planes val buffer planes[0].buffer val pixelStride planes[0].pixelStride val rowStride planes[0].rowStride // 根据Image的格式和参数将数据转换为Bitmap val bitmap convertImageToBitmap(image, pixelStride, rowStride) // 保存Bitmap到文件或进行其他处理 saveBitmapToFile(bitmap) // 重要处理完后关闭Image释放底层资源 image.close() } } catch (e: Exception) { e.printStackTrace() } finally { image?.close() } }convertImageToBitmap函数涉及ByteBuffer到Bitmap的转换需要注意rowStride每行的字节数可能大于width * pixelStride即存在行填充padding直接创建Bitmap时需要正确处理。4. 性能优化与兼容性处理功能实现只是第一步要让功能稳定可用必须关注性能和兼容性。4.1 录制性能优化策略分辨率与帧率权衡不是所有场景都需要1080p/60fps。对于文档演示或普通App操作720p/30fps在文件大小和清晰度上是最佳平衡点。可以通过DisplayMetrics获取屏幕密度然后按比例缩放目标分辨率。码率Bitrate控制setVideoEncodingBitRate是关键。码率太低视频模糊太高则文件巨大。一个经验公式码率 (bps) ≈ 宽度 * 高度 * 帧率 * 运动复杂度因子 * 0.07。对于静态内容多的场景因子可以取0.05对于游戏可能需要0.1或更高。建议提供“高、中、低”质量选项让用户选择。使用SurfaceView预览如果应用内需要预览录制画面务必使用SurfaceView或TextureView而不是将Bitmap设置到ImageView。前者效率高几个数量级。异步处理MediaRecorder.prepare()和start()是阻塞调用必须在后台线程执行否则会导致ANR。4.2 截屏性能与内存管理及时关闭ImageImage对象持有宝贵的本地内存来自ImageReader的缓冲区。必须在try-catch-finally块中确保image.close()被调用否则很快会导致缓冲区耗尽后续截屏失败。ImageReader缓冲区数量maxImages参数设为2双缓冲在绝大多数情况下是最优的。设为1可能导致丢帧大于2则浪费内存。Bitmap复用与压缩如果需要连续截屏考虑复用Bitmap对象。保存文件时根据用途选择压缩格式和质量PNG无损但体积大适合截图存档JPEG有损压缩适合网络传输或预览可通过Bitmap.compress(CompressFormat.JPEG, 85, outputStream)控制质量。4.3 多版本Android系统兼容性API 级别关键变化适配方案Android 5.0 (API 21)引入MediaProjection项目最低支持版本。Android 8.0 (API 26)引入通知渠道(Notification Channel)创建前台服务通知时必须创建对应的渠道。Android 9.0 (API 28)限制后台应用访问麦克风如果录制麦克风音频确保应用在前台。Android 10 (API 29)1. 屏幕捕获必须在前台服务中。2. 引入foregroundServiceType。3. 访问外部存储需要作用域存储权限。1. 确保服务运行在前台。2. 在Manifest和服务启动时指定mediaProjection类型。3. 使用MediaStoreAPI保存视频到Movies目录或保存到App私有目录。Android 11 (API 30)单次权限授予。MediaProjection的授权弹窗更频繁。做好用户可能每次录制都需要授权的心理准备和UX设计。Android 12 (API 31)前台服务启动限制更严格。确保从用户交互如点击按钮直接启动前台服务。Android 13细化媒体权限。如果需要录制音频除了MediaProjection还需申请RECORD_AUDIO权限。针对Android 10的作用域存储 保存录制文件的最佳位置是MediaStore。private fun getOutputMediaFile(): File? { if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { val resolver contentResolver val contentValues ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, recording_${System.currentTimeMillis()}.mp4) put(MediaStore.Video.Media.MIME_TYPE, video/mp4) put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES /YourAppName) } val uri resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues) uri?.let { // 使用 resolver.openOutputStream(it) 获取输出流传递给MediaRecorder return File(it.path) // 注意这不是真正的文件路径是一个Uri } } else { // Android 9及以下使用传统文件路径 val moviesDir Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) return File(moviesDir, YourAppName/recording_${System.currentTimeMillis()}.mp4) } return null }5. 常见问题排查与实战技巧在实际开发中你几乎一定会遇到下面这些问题。5.1 录制启动失败或立即停止问题现象调用mediaRecorder.start()后立即触发onError或录制几秒后自动停止。排查步骤检查前台服务确保你的Service已正确调用startForeground()并且在Android 10声明了foregroundServiceTypemediaProjection。查看通知栏是否有持续的通知。检查权限确保MediaProjection的授权结果resultCode是Activity.RESULT_OK并且data不为null。检查输出路径确保MediaRecorder.setOutputFile()设置的路径是可写的。在Android 10如果路径在外部存储必须使用MediaStoreAPI。检查参数兼容性某些设备对极端参数如过高的分辨率、码率支持不好。尝试使用一组保守的参数如720p, 3Mbps, 30fps进行测试。查看Logcat搜索MediaRecorder和MediaProjection相关的错误日志通常会有明确的失败原因。5.2 截屏获取的Image或Bitmap为null或花屏问题现象ImageReader回调触发了但acquireLatestImage()返回null或转换后的Bitmap是黑的、绿的、扭曲的。排查步骤检查ImageReader配置确保PixelFormat如RGBA_8888与后续Bitmap转换代码匹配。Image的Plane索引planes[0]可能因格式而异。检查rowStride这是最常见的坑。rowStride是缓冲区中每行的字节数它可能为了内存对齐而大于width * pixelStride。创建Bitmap时必须使用rowStride作为行跨度rowBytes。// 错误做法假设没有padding // Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) // 正确做法使用ByteBuffer和rowStride val bitmap Bitmap.createBitmap( width (rowStride - width * pixelStride) / pixelStride, // 调整宽度以容纳padding height, Bitmap.Config.ARGB_8888 ) bitmap.copyPixelsFromBuffer(buffer) // 然后可能需要裁剪掉多余的部分 val finalBitmap Bitmap.createBitmap(bitmap, 0, 0, width, height)及时关闭Image确认在finally块中调用了image.close()否则缓冲区泄漏会导致后续无法获取新图像。VirtualDisplay尺寸确保ImageReader的尺寸和VirtualDisplay的尺寸一致或成比例否则会出现缩放失真。5.3 音频录制相关问题录制不到系统内部声音媒体音在Android 10及以上普通应用无法通过MediaProjection直接录制系统媒体音频。这是出于隐私保护的严格限制。解决方案通常是引导用户开启系统自带的“内部音频录制”功能如果设备支持或者录制麦克风声音。一些系统定制ROM或特定设备可能有不同策略。音频和视频不同步确保MediaRecorder设置的音频采样率如44100Hz、声道数与设备支持的标准一致。不同步通常源于时间戳处理错误或编码器问题使用标准参数能最大程度避免。5.4 用户体验优化技巧授权引导在第一次触发录制/截屏前用清晰的弹窗或页面向用户解释为什么需要这个权限会有什么样的系统弹窗出现“开始截取您的屏幕内容”以及录制时会有常驻通知。这能大幅降低用户的困惑和拒绝率。提供悬浮窗控制录制开始后Activity可能被切到后台。提供一个可移动的悬浮窗控件需要SYSTEM_ALERT_WINDOW权限来显示录制状态、暂停/停止录制是非常专业的做法。处理屏幕旋转屏幕旋转会导致VirtualDisplay的尺寸和密度发生变化。一种处理方式是锁定当前Activity的方向另一种更复杂的方式是监听配置变化动态销毁并重建VirtualDisplay和MediaRecorder/ImageReader。电量与发热提示长时间高分辨率高帧率录制非常耗电和发热。在UI上给出提示并提供降低画质的选项。最后测试时务必覆盖不同品牌、不同Android版本的机型特别是华为、小米、OPPO、vivo等对后台管理和权限有深度定制的系统它们的表现可能与原生Android有差异。

相关新闻