Android Q以上版本,用MediaProjection录屏时遇到的3个坑和我的填坑记录
Android Q+录屏开发实战:MediaProjection避坑指南与高阶优化
在移动应用开发中,屏幕录制功能的需求日益增长——从游戏精彩时刻保存到在线教育演示制作,再到远程协作技术支持。然而,当你的应用目标平台升级到Android Q及以上版本时,原本"能用"的MediaProjection实现可能突然抛出各种SecurityException,让开发者措手不及。本文将深入剖析三个最具代表性的兼容性问题,并提供经过生产环境验证的解决方案。
1. 权限与服务:Android Q+的强制前台服务机制
Android 10引入的隐私保护政策对屏幕捕获行为进行了严格规范。我们首先遭遇的就是这个经典异常:
java.lang.SecurityException: Media projections require a foreground service...1.1 前台服务配置要点
在AndroidManifest.xml中需要声明两项关键配置:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <service android:name=".ScreenCaptureService" android:foregroundServiceType="mediaProjection" android:exported="true"/>常见误区:
- 遗漏
foregroundServiceType声明 - 服务启动后才申请MediaProjection权限
- 通知渠道不符合Android 8.0+要求
1.2 服务启动时序控制
正确的执行顺序应该是:
- 启动前台服务并显示通知
- 获取用户授权(createScreenCaptureIntent)
- 在onActivityResult中初始化MediaProjection
// 错误示例:先申请权限再启动服务 fun startRecording() { val mediaManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult(mediaManager.createScreenCaptureIntent(), REQUEST_CODE) // 此时服务尚未启动,必定抛出异常 } // 正确流程 fun safeStartRecording() { val serviceIntent = Intent(this, CaptureService::class.java) startService(serviceIntent) // 先启动服务 bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE) Handler(Looper.getMainLooper()).postDelayed({ val mediaManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult(mediaManager.createScreenCaptureIntent(), REQUEST_CODE) }, 300) // 确保服务已启动 }提示:Android 12+要求前台服务通知立即显示,延迟超过5秒可能导致ANR
2. 音频捕获:被忽视的RECORD_AUDIO权限
当录屏需要同步录制系统音频时,另一个隐蔽的陷阱正在等待:
java.lang.RuntimeException: setAudioSource failed2.1 动态权限管理策略
除了基本的存储权限,音频采集需要额外处理:
private val REQUIRED_PERMISSIONS = arrayOf( Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE ) fun checkPermissions() { val ungranted = REQUIRED_PERMISSIONS.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } if (ungranted.isNotEmpty()) { ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), PERMISSION_REQUEST_CODE) } else { startRecordingWorkflow() } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { startRecordingWorkflow() } else { showPermissionDeniedDialog() } } }2.2 MediaRecorder配置优化
针对不同Android版本的最佳参数配置:
| 参数项 | Android 5-9 推荐值 | Android 10+ 推荐值 | 注意事项 |
|---|---|---|---|
| 视频编码器 | H.264 | H.265 (HEVC) | 需检查设备支持情况 |
| 音频采样率 | 44.1kHz | 48kHz | 影响音质与文件大小 |
| 关键帧间隔 | 2秒 | 1秒 | 影响视频seek性能 |
| 比特率控制模式 | CQ (恒定质量) | VBR (动态比特率) | 平衡质量与文件大小 |
fun setupMediaRecorder(outputFile: File): MediaRecorder { return MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { setAudioEncoder(MediaRecorder.AudioEncoder.AAC_ELD) setVideoEncoder(MediaRecorder.VideoEncoder.HEVC) setVideoEncodingProfile( MediaCodecInfo.CodecProfileLevel.HEVCProfileMain, MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel31 ) } else { setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setVideoEncoder(MediaRecorder.VideoEncoder.H264) } setOutputFile(outputFile.absolutePath) prepare() } }3. Android 12+的PendingIntent新规
当应用目标API升级到31时,这个运行时崩溃会让许多开发者困惑:
java.lang.IllegalArgumentException: Targeting S+ requires one of FLAG_IMMUTABLE...3.1 兼容性处理方案
通知栏PendingIntent的创建需要区分版本:
fun createNotificationPendingIntent(): PendingIntent { val intent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT } return PendingIntent.getActivity(this, 0, intent, flags) }3.2 虚拟显示配置进阶技巧
Android 12对虚拟显示的行为也有细微调整:
fun createVirtualDisplay( mediaProjection: MediaProjection, width: Int, height: Int, dpi: Int, surface: Surface ): VirtualDisplay { val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION or DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY } else { DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC } return mediaProjection.createVirtualDisplay( "ScreenCapture", width, height, dpi, flags, surface, null, null ) }4. 性能优化与异常处理实战
4.1 内存泄漏防护体系
MediaProjection相关组件必须严格管理生命周期:
class ScreenCaptureService : Service() { private var mediaProjection: MediaProjection? = null private var virtualDisplay: VirtualDisplay? = null override fun onDestroy() { virtualDisplay?.release() mediaProjection?.stop() super.onDestroy() } fun cleanUp() { virtualDisplay?.let { it.release() virtualDisplay = null } mediaProjection?.let { it.stop() mediaProjection = null } } }4.2 帧率稳定方案
通过SurfaceTexture实现帧率控制:
fun setupFrameRateController(width: Int, height: Int): SurfaceTexture { val surfaceTexture = SurfaceTexture(0).apply { setDefaultBufferSize(width, height) } val surface = Surface(surfaceTexture) // 使用Choreographer控制帧采样 val choreographer = Choreographer.getInstance() val callback = object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { surfaceTexture.updateTexImage() // 处理帧数据... if (isRecording) { choreographer.postFrameCallback(this) } } } choreographer.postFrameCallback(callback) return surfaceTexture }4.3 设备兼容性矩阵
不同厂商设备的特殊处理:
| 厂商 | 已知问题 | 解决方案 |
|---|---|---|
| 小米 | 后台服务被杀概率高 | 启用自启动权限引导 |
| 华为 | 虚拟显示黑屏 | 关闭"智能分辨率"设置 |
| OPPO | 音频采集失败 | 使用VOICE_RECOGNITION作为音源 |
| 三星 | 视频编码器不支持HEVC | 自动降级到H.264 |
在真实项目中,我们还需要考虑以下增强功能点:
- 动态码率调整(Network Aware Encoding)
- 多轨道录制(视频+音频+传感器数据同步)
- 低功耗模式(针对长时间录制场景)
- 实时预览与编辑功能集成
通过系统化的异常预防和处理机制,可以显著提升MediaProjection录屏功能的稳定性和用户体验。建议在开发过程中建立完整的设备测试矩阵,特别关注各厂商旗舰机型的行为差异。
