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

Android 13 Media Projection截图偶尔黑屏问题求助

Android 13 MediaProjection 偶尔黑屏问题排查与修复

核心问题分析

你的代码存在几个关键问题,直接导致了偶尔黑屏的现象:

  • 过早停止MediaProjection:在acquireLatestImage()之前就调用mediaProjection.stop(),此时系统可能还没完成帧捕获,直接终止投影会导致获取空帧或黑屏帧。
  • ImageReader配置不合理:maxImages设为1,当系统帧队列有延迟时,旧帧被覆盖但新帧未就绪,容易出现空帧。
  • 资源释放逻辑混乱:重复调用mediaProjection.stop(),且Image的关闭时机晚于部分资源处理,可能引发帧数据异常。
  • 未处理帧获取失败场景:当acquireLatestImage()返回null时,没有对应的清理逻辑,可能导致投影资源泄漏。

针对性修复方案

1. 调整MediaProjection停止时机

必须在成功获取并处理完Image之后,再停止MediaProjection,避免中断帧捕获流程。

2. 优化ImageReader配置

将maxImages调整为2-3,给系统预留帧缓冲空间,减少帧丢失概率。

3. 规范资源释放顺序

确保Image在Bitmap处理完成后立即关闭,避免占用系统帧资源;同时避免重复调用mediaProjection.stop()。

4. 增加异常处理与重试机制

针对Android 13的系统特性,增加帧获取失败的重试逻辑,最多重试2次,避免单次失败直接返回黑屏。

修改后的完整代码

WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getRealSize(size);
final int width = size.x, height = size.y;

// 调整maxImages为2,增加帧缓冲
final ImageReader imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
final int[] retryCount = {0}; // 重试计数器

imageReader.setOnImageAvailableListener(reader -> {
    Image image = null;
    try {
        image = reader.acquireLatestImage();
        if (image == null) {
            // 帧获取失败,重试一次(最多2次)
            if (retryCount[0] < 2) {
                retryCount[0]++;
                // 延迟100ms重试,给系统时间准备帧
                new Handler(Looper.getMainLooper()).postDelayed(() -> {
                    if (mediaProjection != null) {
                        // 重新创建VirtualDisplay触发帧捕获
                        createVirtualDisplay(mediaProjection, imageReader.getSurface(), width, height);
                    }
                }, 100);
            }
            return;
        }

        Image.Plane[] planes = image.getPlanes();
        ByteBuffer buffer = planes[0].getBuffer();
        int pixelStride = planes[0].getPixelStride();
        int rowStride = planes[0].getRowStride();
        int rowPadding = rowStride - pixelStride * width;

        Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(buffer);

        // 裁剪掉padding部分,得到正确尺寸的截图
        Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
        bitmap.recycle(); // 释放原bitmap资源

        String fileName = "Screenshot_" + System.currentTimeMillis() + ".jpg";
        String destinationPath = context.getExternalFilesDir(null) + "/screenshots/" + fileName;

        File imageFile = new File(destinationPath);
        // 确保目录存在
        imageFile.getParentFile().mkdirs();

        try (FileOutputStream outputStream = new FileOutputStream(imageFile)) {
            croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
        }

        String mediaPath = Environment.DIRECTORY_PICTURES + File.separator + "Screenshots/myapp" + File.separator;
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
        values.put(MediaStore.Images.Media.IS_PENDING, 0);
        values.put(MediaStore.Images.Media.RELATIVE_PATH, mediaPath);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        values.put(MediaStore.Images.Media.SIZE, imageFile.length());
        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);

        Uri path = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        if (path != null) {
            try (OutputStream imageOutStream = context.getContentResolver().openOutputStream(path)) {
                croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
            }
        }

        croppedBitmap.recycle(); // 裁剪后的bitmap也及时释放

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 确保Image资源被释放
        if (image != null) {
            image.close();
        }
        // 处理完成后再停止MediaProjection
        if (mediaProjection != null) {
            mediaProjection.stop();
            mediaProjection = null;
        }
        // 关闭ImageReader
        if (reader != null) {
            reader.close();
        }
    }
}, null);

// 单独提取创建VirtualDisplay的方法,方便重试调用
private void createVirtualDisplay(MediaProjection mediaProjection, Surface surface, int width, int height) {
    if (mediaProjection != null) {
        mediaProjection.createVirtualDisplay("ScreenshotDisplay",
                width, height, Resources.getSystem().getDisplayMetrics().densityDpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                surface, null, null);
    }
}

额外注意事项(Android 13适配)

  • 权限检查:确保已经申请POST_NOTIFICATIONS权限(Android 13+要求),否则MediaProjection可能无法正常启动。
  • 后台限制:Android 13对后台启动MediaProjection有更严格的限制,确保截图操作在前台触发(比如用户点击按钮时执行)。
  • Surface生命周期:VirtualDisplay的Surface要和ImageReader的Surface绑定,确保在ImageReader就绪后再创建VirtualDisplay。

内容的提问来源于stack exchange,提问作者Jayce

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 04:25:37