MediaProjection录屏时Service类截图遇OnImageAvailableListener未触发问题
在Service中结合MediaProjection实现截图的解决方案
问题核心
你的代码仅初始化了ImageReader但未将其Surface与MediaProjection关联,导致OnImageAvailableListener无法收到图像回调。当前录屏用的VirtualDisplay仅绑定了MediaRecorder的Surface,并未关联截图用的ImageReader。
解决方案
修改截图逻辑:创建ImageReader后,用MediaProjection生成临时VirtualDisplay绑定其Surface,触发图像采集;截图完成后及时销毁临时资源,避免内存泄漏。同时优化图像处理逻辑适配正确的像素格式。
修改后的完整代码
var imageReader: ImageReader? = null var screenshotVirtualDisplay: VirtualDisplay? = null // 临时截图专用VirtualDisplay // 调用此方法触发截图 private fun captureScreenshot() { val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager val metrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(metrics) val screenDensity = metrics.densityDpi val screenWidth = metrics.widthPixels val screenHeight = metrics.heightPixels // 初始化ImageReader,采用RGBA_8888格式提升兼容性 imageReader = ImageReader.newInstance(screenWidth, screenHeight, ImageFormat.RGBA_8888, 1) imageReader?.setOnImageAvailableListener({ reader -> val image = reader.acquireLatestImage() image?.let { try { processImage(it) } finally { // 强制释放资源,避免内存泄漏 image.close() screenshotVirtualDisplay?.release() screenshotVirtualDisplay = null imageReader?.close() imageReader = null } } }, Handler(Looper.getMainLooper())) // 绑定主线程Handler,确保回调执行线程安全 // 创建临时VirtualDisplay,绑定ImageReader的Surface以接收屏幕画面 imageReader?.surface?.let { surface -> screenshotVirtualDisplay = mMediaProjection?.createVirtualDisplay( "ScreenshotDisplay", screenWidth, screenHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null ) } } private fun processImage(image: Image) { 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,得到正确尺寸的截图 val croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height) saveBitmapToFile(croppedBitmap) // 释放原Bitmap资源 bitmap.recycle() } // 以下方法保持原有逻辑不变 private fun getParentFolder(): String { return "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)}/${ getString(R.string.app_name) }" } private fun saveBitmapToFile(bitmap: Bitmap) { Log.d("de_save", "saveBitmapToFile: ") try { val savePath = getFolder() Log.d("de_save", "saveBitmapToFile: savepath: $savePath") val file = File(savePath, "ScreenShot_${System.currentTimeMillis()}") val fileOutputStream = FileOutputStream(file) bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream) fileOutputStream.close() ToastUtils.showToast("Screenshot Taken") } catch (e: IOException) { e.printStackTrace() } } private fun getFolder(): String { val location = getParentFolder() + File.separator + "ScreenShot" val file = File(location) if (!file.exists()) { file.mkdirs() } return location }
关键注意事项
- 权限校验:确保已申请
MEDIA_PROJECTION权限,Android 10及以上需适配Scoped Storage或申请MANAGE_EXTERNAL_STORAGE权限。 - 资源释放:每次截图完成后必须关闭
Image、销毁临时VirtualDisplay、关闭ImageReader,避免内存泄漏。 - 线程安全:回调逻辑绑定主线程Handler,避免后台线程操作UI或引发线程异常。
内容的提问来源于stack exchange,提问作者Syed Faisal Naqvi
相关产品推荐
相关产品推荐

