Android屏幕录制与截图开发指南:MediaProjection与View.draw()实战 1. 项目概述从需求到实现的完整路径在移动应用开发中屏幕录制和截屏功能正变得越来越普遍。无论是用于创建应用演示教程、用户反馈报告还是实现一些创意性的交互功能这两个能力都构成了现代应用体验的重要一环。对于Android开发者而言实现这两个功能不仅仅是调用几个API那么简单它涉及到权限管理、服务生命周期、UI交互、性能优化以及不同Android版本的兼容性等一系列复杂问题。我见过不少项目初期只是简单堆砌代码后期在适配、稳定性和用户体验上栽了大跟头。今天我就结合自己多年的踩坑经验为你梳理一套从设计到实现的完整方案涵盖从Android 5.0到最新版本的核心技术要点和避坑指南目标是让你写出的功能既健壮又高效。2. 核心方案选型与架构设计2.1 屏幕录制方案深度对比在Android上实现屏幕录制主流有三种技术路径选择哪一种取决于你的目标API级别、功能需求以及对系统依赖的容忍度。方案一MediaProjection API (Android 5.0)这是目前最主流、最推荐的方式。它通过MediaProjectionManager创建一个虚拟的“投影”捕获整个屏幕或指定窗口的帧然后通过MediaRecorder或MediaCodec编码成视频文件。它的优势在于官方支持、功能强大、可以录制系统声音需额外权限并且从Android 10开始支持录制内部音频。但它的缺点是需要用户弹窗确认无法完全静默开启并且对性能有一定影响。方案二adb shell screenrecord 命令这其实是一个调试工具通过执行adb shell screenrecord /sdcard/demo.mp4命令来录制。在应用内可以通过Runtime.getRuntime().exec()来调用。这个方案极其简单但缺点非常明显需要WRITE_SECURE_SETTINGS或DUMP等高级系统权限普通应用根本无法获取因此几乎只适用于系统应用或拥有root权限的设备对绝大多数第三方应用来说不可行。方案三通过辅助功能服务(AccessibilityService)模拟点击“截屏”这是一种取巧的方法通过辅助功能服务监听全局手势或按键事件当触发录制条件时模拟按下系统截屏组合键如电源音量下然后利用系统自带的录屏功能。这种方法完全依赖设备厂商的系统实现不同品牌手机弹出的录屏UI和操作逻辑千差万别稳定性极差且无法自定义输出格式、分辨率或进行应用内精细控制不推荐用于正式产品。实操心得无脑选择MediaProjection方案。虽然它需要用户授权但这恰恰符合Android系统的安全规范。我们应该把弹窗视为一个与用户建立信任的契机通过清晰的说明引导用户授权而不是想方设法绕过它。2.2 截屏方案深度对比截屏的实现同样有多条路复杂程度和适用场景各不相同。方案一MediaProjection 创建虚拟显示并捕获单帧这是最强大、最灵活的方案与录屏同源。通过MediaProjection创建ImageReader可以获取到原始的Image对象进而转换成Bitmap。你可以精确控制截取哪个屏幕、什么分辨率甚至可以实现超高速连续截图。但它的代价是重需要一套类似录屏的权限和生命周期管理。方案二View.draw(Canvas) 方法对于截取当前应用自身窗口的内容这是最简单高效的方法。原理是让View将其UI层次结构绘制到一个你提供的Canvas上而这个Canvas关联着一个Bitmap。这种方法性能极佳无需任何权限但只能截取自己应用的视图无法截取系统状态栏、导航栏或其他应用。fun captureView(view: View): Bitmap? { if (view.width 0 || view.height 0) return null val bitmap Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) view.draw(canvas) return bitmap }方案三反射调用 SurfaceControl.screenshot()这是一个隐藏的底层API。在Android系统中有一个SurfaceControl类其静态方法screenshot()可以截取整个屏幕。通过Java反射可以调用它。这种方法速度快能截取全屏但在不同版本Android上方法签名可能变化且从Android 10开始对非系统应用的限制越来越严格可能直接返回null或抛出安全异常稳定性是最大问题。方案四辅助功能服务(AccessibilityService)执行全局手势和取巧的录屏方案类似通过辅助功能服务执行一个“下滑通知栏并点击截屏按钮”的全局手势序列。这同样严重依赖厂商定制失败率高体验怪异仅作为最后的研究手段。注意事项如果你的应用只需要截取自身界面View.draw()是最佳选择。如果需要截取包括状态栏在内的全屏且应用可能安装在Android 10及以上设备那么MediaProjection是唯一可靠的选择。务必放弃对反射等黑魔法的幻想它们带来的维护成本远高于其暂时带来的便利。2.3 整体架构设计思路一个健壮的屏幕录制与截屏模块不应该把代码全部堆在Activity里。我推荐采用基于Service的架构特别是Foreground Service前台服务。为什么必须用前台服务屏幕录制是一个长时间运行的后台任务。如果放在Activity中一旦用户切到后台或屏幕关闭Activity可能被销毁录制线程也会随之终止。而Service具有独立的后台生命周期。从Android 8.0开始后台服务限制非常严格长时间运行的任务必须启动为前台服务并显示一个持续的通知否则系统很快会停止它。对于录屏这种用户明确感知的任务显示通知是合乎情理的。核心组件职责划分权限请求与初始化 (Activity)负责向用户申请Manifest.permission.RECORD_AUDIO如果需要录音和通过MediaProjectionManager.createScreenCaptureIntent()启动授权弹窗。处理授权结果。录制/截图核心引擎 (Foreground Service)在Service中持有MediaProjection对象管理MediaRecorder或ImageReader的生命周期执行实际的编码、写入文件等耗时操作。服务通过Notification保持前台状态。控制与状态通信通过BroadcastReceiver、LiveData或EventBus等机制在Service和UIActivity或Fragment之间传递控制命令开始/停止和状态更新录制时长、文件保存路径等。数据存储与回调Service将处理好的视频文件或图片保存到应用专属目录或公共媒体目录并通过回调通知UI层结果。这样的架构解耦清晰生命周期管理稳妥能有效应对应用切换、配置变更等复杂场景。3. 屏幕录制的分步实现与核心代码解析3.1 权限声明与动态申请首先在AndroidManifest.xml中声明必要的权限。注意RECORD_AUDIO权限在Android 13及以上版本需要动态申请。uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.RECORD_AUDIO / !-- 从Android 10开始录制内部音频需要此权限 -- uses-permission android:nameandroid.permission.CAPTURE_AUDIO_OUTPUT / !-- 写入外部存储用于保存文件 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- Android 10及以上使用媒体存储API可能不需要WRITE_EXTERNAL_STORAGE --在Activity中你需要按顺序申请权限和MediaProjection授权。这是一个关键流程顺序错了会导致体验不佳。class RecordActivity : AppCompatActivity() { private lateinit var mediaProjectionManager: MediaProjectionManager private val requestCodeMediaProjection 100 private val requestCodeAudioPermission 101 fun startRecordProcess() { // 1. 首先检查并申请录音权限如果需要音轨 if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), requestCodeAudioPermission) return // 等待权限回调 } } // 2. 录音权限已有则申请屏幕捕获授权 requestScreenCapture() } override fun onRequestPermissionsResult(requestCode: Int, permissions: Arrayout String, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode requestCodeAudioPermission grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { // 录音权限获取成功继续申请屏幕捕获 requestScreenCapture() } else { // 处理权限被拒绝的情况 Toast.makeText(this, 录音权限被拒绝无法录制声音, Toast.LENGTH_SHORT).show() // 这里可以根据业务决定是否继续申请仅录屏 requestScreenCapture() } } private fun requestScreenCapture() { mediaProjectionManager getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager val captureIntent mediaProjectionManager.createScreenCaptureIntent() startActivityForResult(captureIntent, requestCodeMediaProjection) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode requestCodeMediaProjection resultCode Activity.RESULT_OK data ! null) { // 3. 授权成功获取MediaProjectionToken并启动录制服务 val projection mediaProjectionManager.getMediaProjection(resultCode, data) startRecordService(projection) } } private fun startRecordService(mediaProjection: MediaProjection) { val intent Intent(this, ScreenRecordService::class.java).apply { putExtra(media_projection_token, mediaProjection) // 可以传递其他参数如分辨率、码率等 putExtra(width, 1080) putExtra(height, 1920) putExtra(bitrate, 6000000) // 6 Mbps } if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) } } }踩坑实录createScreenCaptureIntent()返回的Intent必须用startActivityForResult启动用startActivity是没用的。授权结果data非常重要后续获取MediaProjection对象必须用到它不能只凭resultCode。3.2 前台服务与MediaRecorder配置接下来是重头戏在Service中配置并启动录制。这里以使用MediaRecorder为例因为它API相对简单适合大多数场景。如果需要更底层的控制如实时处理帧数据则需使用MediaCodec。class ScreenRecordService : Service() { private lateinit var mediaProjection: MediaProjection private lateinit var mediaRecorder: MediaRecorder private lateinit var virtualDisplay: VirtualDisplay private var screenWidth 1080 private var screenHeight 1920 private var screenDensity resources.displayMetrics.densityDpi override fun onCreate() { super.onCreate() mediaRecorder MediaRecorder() // 必须创建前台通知否则在Android 8.0上服务会被快速停止 createNotificationChannel() startForeground(NOTIFICATION_ID, buildRecordingNotification()) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.let { val token it.getIBinderExtra(media_projection_token) mediaProjection MediaProjectionManager.getMediaProjection(this, token) screenWidth it.getIntExtra(width, 1080) screenHeight it.getIntExtra(height, 1920) startRecording() } return START_STICKY } private fun startRecording() { try { // 1. 配置MediaRecorder这是一个严格的顺序流程 mediaRecorder.apply { setAudioSource(MediaRecorder.AudioSource.MIC) // 录制麦克风声音 // 如果想录制系统内部音频Android 10上述方案不行需要更复杂配置 setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(44100) // 标准采样率 setAudioEncodingBitRate(128000) // 128 kbps setVideoEncoder(MediaRecorder.VideoEncoder.H264) setVideoEncodingBitRate(6000000) // 6 Mbps根据分辨率调整 setVideoFrameRate(30) // 帧率 setVideoSize(screenWidth, screenHeight) // 设置输出文件路径 val outputDir if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 使用MediaStore getExternalFilesDir(Environment.DIRECTORY_MOVIES)?.absolutePath } else { Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).absolutePath } val outputFile File(outputDir, screen_record_${System.currentTimeMillis()}.mp4) setOutputFile(outputFile.absolutePath) prepare() // 这个调用可能抛出IOException必须try-catch } // 2. 创建VirtualDisplay将屏幕内容投射到MediaRecorder的Surface virtualDisplay mediaProjection.createVirtualDisplay( ScreenRecorder, screenWidth, screenHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mediaRecorder.surface, // 关键将Recorder的Surface作为显示目标 null, null ) // 3. 开始录制 mediaRecorder.start() updateNotification(正在录制...) // 更新通知内容 } catch (e: Exception) { Log.e(TAG, 启动录制失败, e) stopSelf() // 启动失败停止服务 } } private fun stopRecording() { try { mediaRecorder.apply { stop() reset() // 重置到空闲状态便于下次使用 } virtualDisplay.release() mediaProjection.stop() } catch (e: Exception) { Log.e(TAG, 停止录制失败, e) } finally { stopForeground(true) stopSelf() } } // ... 创建通知渠道、构建通知等方法省略 companion object { private const val NOTIFICATION_ID 1001 private const val CHANNEL_ID screen_record_channel } }关键参数解析VideoSize: 设置的分辨率最好与VirtualDisplay创建的分辨率一致否则会引发缩放影响性能和质量。可以通过DisplayMetrics获取真实屏幕分辨率但录制4K视频对设备压力很大通常取1080P是一个平衡点。BitRate码率: 这是影响视频文件大小和清晰度的核心参数。计算公式可参考码率 ≈ 分辨率宽 × 分辨率高 × 帧率 × 每像素比特数 × 压缩系数。对于H.264编码的1080P 30fps视频4-8 Mbps是常见范围。码率太低会模糊太高则文件巨大。OutputFormat:MPEG_4是最通用的容器格式。OutputFormat.THREE_GPP等格式兼容性可能有问题。输出路径: Android 10API 29引入了分区存储Scoped Storage直接访问外部存储根目录需要特殊权限。推荐做法是Android 10以下使用Environment.getExternalStoragePublicDirectoryAndroid 10及以上使用Context.getExternalFilesDir()或MediaStoreAPI。上面的示例做了简单版本判断更健壮的做法是统一使用MediaStore。实操心得MediaRecorder的prepare()方法必须被调用且必须在配置完所有参数之后、start()之前调用。它的调用可能因为参数不兼容、路径不可写等原因抛出IOException务必进行异常捕获。一个常见的坑是在prepare()之后又去修改参数如setOutputFile这是无效的。3.3 处理音频录制系统内部音频从Android 10开始MediaProjection增加了录制系统内部音频的能力但这不再是简单的setAudioSource(MediaRecorder.AudioSource.MIC)。你需要使用AudioPlaybackCaptureConfigurationAPI。这个过程相当复杂核心步骤是在创建MediaRecorder前构建一个AudioPlaybackCaptureConfiguration通过AudioManager获取允许捕获的音频源列表。将此配置设置给MediaRecorder。动态申请CAPTURE_AUDIO_OUTPUT权限。由于该API的复杂性和版本碎片化很多应用选择只录制麦克风声音或者通过无障碍服务模拟“录制系统音频”的选项这又回到了兼容性噩梦。如果你的应用必须录制纯净系统音需要投入大量精力进行兼容性测试。4. 屏幕截图的精准实现方案4.1 基于MediaProjection的截图实现如果你已经通过MediaProjection授权那么用它来截图是最强大的。与录制视频不同截图不需要MediaRecorder而是使用ImageReader来接收单帧图像。class ScreenshotCaptureService : Service() { private lateinit var mediaProjection: MediaProjection private lateinit var imageReader: ImageReader private var screenDensity resources.displayMetrics.densityDpi fun captureScreenshot(projection: MediaProjection, width: Int, height: Int): Bitmap? { this.mediaProjection projection // 1. 创建ImageReader用于接收屏幕帧 // 参数宽度高度像素格式如RGBA_8888最大图像数量队列深度 imageReader ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) // 2. 创建VirtualDisplay将屏幕内容投射到ImageReader的Surface val virtualDisplay mediaProjection.createVirtualDisplay( ScreenshotCapture, width, height, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.surface, // 关键Surface来自ImageReader null, null ) // 3. 从ImageReader中获取最新的一帧图像 var bitmap: Bitmap? null try { val image imageReader.acquireLatestImage() ?: return null val planes image.planes val buffer planes[0].buffer val pixelStride planes[0].pixelStride val rowStride planes[0].rowStride val rowPadding rowStride - pixelStride * width // 4. 将Image数据转换为Bitmap bitmap Bitmap.createBitmap(width rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888) bitmap.copyPixelsFromBuffer(buffer) image.close() // 5. 裁剪掉可能因内存对齐产生的多余padding部分 if (rowPadding 0) { bitmap Bitmap.createBitmap(bitmap, 0, 0, width, height) } } catch (e: Exception) { Log.e(TAG, 截图失败, e) } finally { virtualDisplay.release() imageReader.close() } return bitmap } }关键点解析ImageReader.newInstance: 第三个参数maxImages设为2就够了一个用于显示一个用于排队避免阻塞。rowStride与rowPadding: 这是图像处理中常见的“内存对齐”问题。rowStride是每一行在内存中实际占用的字节数可能比width * pixelStride大。直接创建Bitmap会导致图像错位必须根据rowStride计算实际宽度或进行裁剪。性能与内存acquireLatestImage()会获取队列中最新的图像并丢弃旧的适合单次截图。如果需要连续截图如做屏幕监控应使用acquireNextImage()并妥善管理Image对象的生命周期及时close()否则会导致内存泄漏和ImageReader队列阻塞。4.2 基于View.draw()的应用内截图对于应用内截图方法就简单直接得多性能也最好。/** * 截取指定View的Bitmap支持超出屏幕的部分如ScrollView * param view 需要截图的View * return 截图Bitmap可能为null */ fun captureView(view: View): Bitmap? { // 确保View已经完成布局测量 if (view.width 0 || view.height 0) { view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)) view.layout(0, 0, view.measuredWidth, view.measuredHeight) } val width view.width val height view.height if (width 0 || height 0) return null val bitmap Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) // 设置背景色避免透明背景变成黑色 canvas.drawColor(Color.WHITE) view.draw(canvas) return bitmap } /** * 截取整个ScrollView或ListView等可滚动视图的内容 */ fun captureScrollView(scrollView: ScrollView): Bitmap? { // 1. 临时将整个内容的高度设置为可见高度以便测量总高度 val originalHeight scrollView.getChildAt(0).height scrollView.getChildAt(0).layoutParams.height LinearLayout.LayoutParams.WRAP_CONTENT // 2. 重新测量并布局获取总内容高度 scrollView.measure(View.MeasureSpec.makeMeasureSpec(scrollView.width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)) val totalHeight scrollView.measuredHeight // 3. 恢复原始高度可选取决于你是否需要保持原状 scrollView.getChildAt(0).layoutParams.height originalHeight scrollView.requestLayout() // 4. 创建足够大的Bitmap val bitmap Bitmap.createBitmap(scrollView.width, totalHeight, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) canvas.drawColor(Color.WHITE) // 5. 保存当前滚动位置 val scrollY scrollView.scrollY // 6. 将ScrollView滚动到顶部开始绘制 scrollView.scrollTo(0, 0) scrollView.draw(canvas) // 7. 绘制完成后恢复滚动位置 scrollView.scrollTo(0, scrollY) return bitmap }注意事项View.draw()方法截图的是当前视图树在内存中的状态而不是屏幕上实际渲染的像素。这意味着如果View的一部分被其他视图如对话框遮挡或者因为硬件加速导致某些效果未同步到软件渲染层截出来的图可能和屏幕显示有细微差别。对于绝大多数UI截图需求这已经足够精确。4.3 截图后的处理与保存获取到Bitmap只是第一步通常还需要压缩、保存到文件或分享。fun saveBitmapToFile(bitmap: Bitmap, context: Context): Uri? { // 压缩并保存为JPEG val bytes ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 90, bytes) // 90%质量 val fileName screenshot_${System.currentTimeMillis()}.jpg val contentValues ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(MediaStore.MediaColumns.MIME_TYPE, image/jpeg) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES /YourAppName) } } return try { val resolver context.contentResolver val uri resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) uri?.let { resolver.openOutputStream(it)?.use { outputStream - outputStream.write(bytes.toByteArray()) } } uri // 返回保存后的Uri可用于分享或显示 } catch (e: IOException) { Log.e(TAG, 保存图片失败, e) null } }使用MediaStoreAPI进行保存是Android 10的推荐做法它不需要WRITE_EXTERNAL_STORAGE权限在Android 10及以上并且文件会出现在系统的相册或文件管理器中用户体验更好。5. 性能优化、兼容性与常见问题排查5.1 性能优化关键点屏幕录制和截图都是资源密集型操作不当实现会导致应用卡顿、发热甚至崩溃。1. 分辨率与帧率权衡不要盲目追求最高分辨率。录制1080P视频比4K视频对CPU和编码器的压力小得多。对于大多数移动设备屏幕分享场景1080P甚至720P已经足够清晰。帧率也是如此30fps是流畅的基准线60fps会显著增加处理负担和文件大小。可以通过DisplayMetrics获取设备屏幕真实分辨率然后动态选择一个适中的录制分辨率。fun getOptimalRecordSize(displayMetrics: DisplayMetrics): PairInt, Int { val screenWidth displayMetrics.widthPixels val screenHeight displayMetrics.heightPixels // 策略如果屏幕分辨率超过1080P则按比例缩放到1080P的短边 val maxEdge 1080 return if (screenHeight maxEdge || screenWidth maxEdge) { val scale maxEdge / screenHeight.toFloat().coerceAtMost(screenWidth.toFloat()) Pair((screenWidth * scale).toInt(), (screenHeight * scale).toInt()) } else { Pair(screenWidth, screenHeight) } }2. 编码参数调优关键帧间隔GOPMediaRecorder默认可能不暴露此设置。如果使用MediaCodec可以设置KEY_I_FRAME_INTERVAL。较小的间隔如1秒有利于视频 seeking但会增大文件较大的间隔如10秒压缩率高但拖动时可能卡顿。Profile 和 Level对于H.264使用Baseline Profile兼容性最好但压缩率低。Main Profile或High Profile压缩率高但旧设备可能不支持。在MediaFormat中设置KEY_PROFILE和KEY_LEVEL。3. 内存与资源管理及时释放VirtualDisplay,ImageReader,Image,MediaRecorder,MediaProjection都是重量级对象必须在onDestroy()或任务结束时调用release(),stop(),close()。Bitmap回收虽然Kotlin/Java有垃圾回收但Bitmap占用的是Native内存回收不及时容易引发OOM。对于大的、不再使用的Bitmap主动调用recycle()并置空引用。使用BitmapFactory.Options.inSampleSize如果截图只是为了生成缩略图可以先通过inSampleSize进行下采样大幅减少内存占用。5.2 不同Android版本的兼容性处理Android的权限和存储模型变化是兼容性问题的重灾区。Android 6.0 (API 23): 动态权限RECORD_AUDIO权限需要动态申请。务必在Activity中处理onRequestPermissionsResult回调。Android 8.0 (API 26): 后台执行限制与通知渠道长时间后台运行的Service必须设置为前台服务并显示通知。同时必须创建通知渠道。Android 10 (API 29): 分区存储与音频捕获存储WRITE_EXTERNAL_STORAGE权限作用域缩小。优先使用MediaStoreAPI或Context.getExternalFilesDir()。音频录制系统音频需使用新的AudioPlaybackCaptureConfigurationAPI并申请CAPTURE_AUDIO_OUTPUT权限。Android 11 (API 30): 软件包可见性如果你的应用需要与其他应用交互例如检测前台应用以决定是否录制可能需要在中声明queries元素。Android 12 (API 31): 前台服务启动限制从后台启动前台服务的限制更加严格。确保你的前台服务启动逻辑是从用户明确的UI操作如点击按钮触发的。一个健壮的兼容性检查与处理示例fun checkAndRequestPermissions(activity: Activity) { val permissionsNeeded mutableListOfString() // 录音权限 if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { if (activity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) ! PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.RECORD_AUDIO) } } // Android 13 需要单独申请通知权限这里只是示例录屏通知通常属于豁免类别但最好检查 if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { if (activity.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ! PackageManager.PERMISSION_GRANTED) { // 根据Google政策用于用户主动触发的操作如录屏的前台服务通知可能不需要此权限但建议申请以保证兼容性 permissionsNeeded.add(Manifest.permission.POST_NOTIFICATIONS) } } if (permissionsNeeded.isNotEmpty()) { activity.requestPermissions(permissionsNeeded.toTypedArray(), PERMISSION_REQUEST_CODE) } else { // 权限已具备继续后续流程如申请MediaProjection activity.requestScreenCapture() } }5.3 常见问题排查速查表下表列出了开发过程中最常见的问题、可能的原因及解决方案。问题现象可能原因排查步骤与解决方案录制开始立即崩溃1.MediaRecorder参数配置顺序错误或参数不兼容。2. 输出文件路径不可写。3.prepare()调用异常未捕获。1. 检查MediaRecorder配置顺序音视频源-格式-编码器-输出文件-prepare。2. 检查输出目录是否存在应用是否有写入权限Android 10注意分区存储。3. 用try-catch包裹prepare()和start()打印详细日志。录制的视频没有声音1. 未申请RECORD_AUDIO权限。2.setAudioSource()未调用或参数错误。3. 某些设备麦克风被其他应用占用。1. 确认动态权限已授予。2. 确认调用了setAudioSource(MediaRecorder.AudioSource.MIC)。3. 尝试在安静环境下录制或检查系统录音权限设置。录制的视频播放时绿屏或花屏1. 视频编码参数分辨率、码率与设备硬件编码器不兼容。2.VirtualDisplay的分辨率与MediaRecorder设置的VideoSize不一致。1. 降低分辨率或码率使用更通用的编码参数组合如H.264 Baseline Profile。2. 确保createVirtualDisplay的宽高与setVideoSize的宽高完全一致。截图获取的Bitmap为null或全黑1.View未完成布局测量宽高为0。2. 使用MediaProjection截图时ImageReader未成功接收到帧。3. 硬件加速导致View.draw()内容异常。1. 在View.post{}中执行截图或手动调用measure和layout。2. 检查MediaProjection授权是否有效VirtualDisplay是否创建成功。3. 尝试在截图前对View调用setLayerType(View.LAYER_TYPE_SOFTWARE, null)临时禁用硬件加速完成后恢复。前台服务通知不显示或录制被系统杀死1. 未正确创建通知渠道Android 8.0。2. 通知的importance太低被系统折叠。3. 服务未调用startForeground()或调用太晚。1. 确保在startForeground前使用NotificationManager.createNotificationChannel创建渠道。2. 将通知渠道的重要性设置为IMPORTANCE_LOW以上。3. 在Service的onStartCommand中尽快调用startForeground必须在onCreate或onStartCommand中调用。在Android 10上无法保存文件到相册使用了旧的Environment.getExternalStoragePublicDirectory方法该方法在Android 10上失效。改用MediaStoreAPI。通过ContentResolver.insert向MediaStore.Images.Media.EXTERNAL_CONTENT_URI或MediaStore.Video.Media.EXTERNAL_CONTENT_URI插入一条记录并获取OutputStream进行写入。用户授权录屏后再次进入应用仍需授权MediaProjection对象是临时的应用进程被杀死后失效。授权结果Intent需要持久化存储。将授权结果Intent即onActivityResult中的data用LocalBroadcastManager发送给后台服务或将其关键信息如resultCode和data的Extras序列化存储到SharedPreferences下次启动服务时尝试用MediaProjectionManager.getMediaProjection重新获取。注意这不是100%可靠系统可能清理因此UI上仍需准备重新授权的流程。一个高级技巧处理屏幕旋转在录制过程中如果设备屏幕旋转VirtualDisplay的内容可能会出现问题。一个解决方案是在Service中监听配置变化当屏幕旋转时释放当前的VirtualDisplay和MediaRecorder然后以新的屏幕方向重新创建并启动它们。这涉及到保存当前的录制状态到临时文件然后拼接逻辑较为复杂。更简单的做法是在AndroidManifest.xml中为录制Activity固定屏幕方向或者在开始录制时动态锁定方向。实现屏幕录制和截屏功能就像在Android系统的边界上进行一场精细的舞蹈。你需要深刻理解权限、服务、显示系统和媒体框架之间的交互。从简单的View.draw()截图到复杂的全系统MediaProjection录制每一层都有其特定的使用场景和陷阱。我个人的经验是在开始编码前先用纸笔画出组件生命周期和数据流向图对于关键步骤如MediaRecorder的参数配置顺序写成固定的代码模板对于兼容性问题建立一个从Android 5.0到最新版的真机测试矩阵。最后永远把用户体验放在首位——清晰的授权提示、稳定的录制过程、及时的状态反馈和合理的资源占用这些才是让用户觉得你的功能“好用”的关键而不仅仅是“能用”。