You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 06:17:28