使用MediaProjectionManager实现前台截图遇问题求助
解决MediaProjectionManager截图黑屏/崩溃问题及语音触发截图实现
一、现有代码的核心问题
- Intent参数引用错误:
startMediaProjection()中直接使用onStartCommand的局部intent变量,服务重启或延迟调用时会导致空指针,需将授权参数持久化存储。 - Image转Bitmap逻辑缺陷:未正确处理图像缓冲区的行间距(rowPadding),导致生成的Bitmap包含无效空白区域或直接黑屏。
- 虚拟显示无状态回调:
createVirtualDisplay未设置回调,无法确认虚拟显示是否创建成功,可能导致ImageReader无法接收画面。 - 前台服务未合规实现:Android 8.0+后台服务无法稳定持有MediaProjection,必须启动前台服务并显示通知。
- 触发逻辑不符合需求:当前代码持续监听截图,而非语音指令触发单次截图。
二、修正后的完整代码
ScreenshotService.kt
class ScreenshotService : Service() { private lateinit var mediaProjectionManager: MediaProjectionManager private var mediaProjection: MediaProjection? = null private var imageReader: ImageReader? = null private var speechRecognizer: SpeechRecognizer? = null private var resultCode: Int = -1 private var data: Intent? = null private val notificationId = 1001 override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { super.onCreate() mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager // 启动前台服务,符合Android 8.0+权限要求 startForeground(notificationId, createForegroundNotification()) // 初始化语音识别 initSpeechRecognizer() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 保存MediaProjection授权参数 intent?.let { resultCode = it.getIntExtra("resultCode", -1) data = it.getParcelableExtra("data") } return START_STICKY } private fun createForegroundNotification(): Notification { val channelId = "screenshot_service_channel" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(channelId, "截图服务", NotificationManager.IMPORTANCE_LOW) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } return NotificationCompat.Builder(this, channelId) .setContentTitle("语音截图服务") .setContentText("说出\"screenshot\"触发截图") .setSmallIcon(R.drawable.ic_notification) .build() } private fun initSpeechRecognizer() { if (!SpeechRecognizer.isRecognitionAvailable(this)) { Toast.makeText(this, "当前设备不支持语音识别", Toast.LENGTH_SHORT).show() stopSelf() return } speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this) val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ENGLISH) speechRecognizer?.setRecognitionListener(object : RecognitionListener { override fun onReadyForSpeech(params: Bundle?) {} override fun onBeginningOfSpeech() {} override fun onRmsChanged(rmsdB: Float) {} override fun onBufferReceived(buffer: ByteArray?) {} override fun onEndOfSpeech() {} override fun onError(error: Int) {} override fun onResults(results: Bundle?) { results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.let { matches -> if (matches.any { it.equals("screenshot", ignoreCase = true) }) { triggerScreenshot() } // 继续监听下一次语音指令 speechRecognizer?.startListening(intent) } } override fun onPartialResults(partialResults: Bundle?) {} override fun onEvent(eventType: Int, params: Bundle?) {} }) // 开始监听语音 speechRecognizer?.startListening(intent) } private fun triggerScreenshot() { if (resultCode == -1 || data == null) { Toast.makeText(this, "请先授权屏幕录制权限", Toast.LENGTH_SHORT).show() return } // 初始化MediaProjection(每次截图前确保实例有效) mediaProjection?.stop() mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!) val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager val metrics = DisplayMetrics() windowManager.defaultDisplay.getRealMetrics(metrics) // 使用getRealMetrics获取真实屏幕尺寸 val width = metrics.widthPixels val height = metrics.heightPixels val densityDpi = metrics.densityDpi // 创建ImageReader,格式使用PRIV格式更可靠 imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1) mediaProjection?.createVirtualDisplay( "ScreenshotVirtualDisplay", width, height, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader?.surface, object : VirtualDisplay.Callback() { override fun onDisplayPaused() {} override fun onDisplayResumed() {} override fun onDisplayStopped() {} }, Handler(Looper.getMainLooper()) ) // 延迟获取图像,确保虚拟显示已渲染完成 Handler(Looper.getMainLooper()).postDelayed({ captureSingleScreenshot() }, 200) } private fun captureSingleScreenshot() { val image = imageReader?.acquireLatestImage() ?: return val bitmap = imageToBitmap(image) saveBitmapToFile(bitmap) // 释放资源 image.close() imageReader?.close() mediaProjection?.stop() Toast.makeText(this, "截图已保存", Toast.LENGTH_SHORT).show() } private fun imageToBitmap(image: Image): Bitmap { val planes = image.planes val buffer = planes[0].buffer val pixelStride = planes[0].pixelStride val rowStride = planes[0].rowStride val rowPadding = rowStride - pixelStride * image.width // 创建正确尺寸的Bitmap val bitmap = Bitmap.createBitmap( image.width + rowPadding / pixelStride, image.height, Bitmap.Config.ARGB_8888 ) bitmap.copyPixelsFromBuffer(buffer) // 裁剪掉多余的行padding return Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height) } private fun saveBitmapToFile(bitmap: Bitmap) { val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) val file = File(storageDir, "screenshot_${System.currentTimeMillis()}.png") try { FileOutputStream(file).use { fos -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) } } catch (e: IOException) { e.printStackTrace() Toast.makeText(this, "保存截图失败", Toast.LENGTH_SHORT).show() } } override fun onDestroy() { super.onDestroy() speechRecognizer?.destroy() mediaProjection?.stop() imageReader?.close() } }
必要的权限声明(AndroidManifest.xml)
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" android:minSdkVersion="33" /> <application ...> <service android:name=".ScreenshotService" android:foregroundServiceType="mediaProjection" /> </application>
启动服务的Activity代码示例
class MainActivity : AppCompatActivity() { private val REQUEST_CODE_SCREEN_CAPTURE = 100 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn_start_service).setOnClickListener { val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_SCREEN_CAPTURE && resultCode == RESULT_OK) { val intent = Intent(this, ScreenshotService::class.java) intent.putExtra("resultCode", resultCode) intent.putExtra("data", data) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) } } } }
三、关键修改说明
- 授权参数持久化:在
onStartCommand中保存resultCode和data,避免服务重启后参数丢失。 - 修复Bitmap转换:添加裁剪步骤去除行padding,使用
getRealMetrics获取真实屏幕尺寸,避免截图区域不全。 - 前台服务合规:创建通知渠道并启动前台服务,符合Android 8.0+后台服务限制。
- 语音触发逻辑:集成
SpeechRecognizer监听"screenshot"指令,触发单次截图后立即释放MediaProjection资源,避免内存泄漏。 - 资源生命周期管理:在
onDestroy和截图完成后及时释放MediaProjection、ImageReader等资源,防止崩溃和内存泄漏。 - 虚拟显示回调:添加
VirtualDisplay.Callback,可扩展处理显示状态变化的逻辑。
内容的提问来源于stack exchange,提问作者Droid Automations
相关产品推荐
相关产品推荐

