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

解决MediaProjection+ImageReader无法获取超过2个图像缓冲区问题

问题

我基于Android的MediaProjection API开发了一款通过TCP/IP实现的无线投屏应用,使用ImageReader捕获客户端屏幕图像缓冲区并发送至服务器。但设置ImageReader的maxImages为2时,仅能成功发送2张图像,尝试获取第3张图像时触发报错:

"Unable to acquire a buffer item, very likely client tried to acquire more than maxImages buffers"
"java.lang.IllegalStateException: maxImages (2) has already been acquired..."

原代码

public class MediaProjectionService extends Service {

    private static final String TAG = MediaProjectionService.class.getSimpleName();
    private static final String CHANNEL_ID = "MediaProjectionChannel";
    private static MediaProjection mediaProjection;
    private ImageReader imageReader;
    private ImageWriter imageWriter;
    private String serverIp;
    private int width, height;
    private static Handler handler;
    private List<Image> imageList = new ArrayList<>();
    private static final int IMAGE_BATCH_SIZE = 10;
    private boolean isFirstImage = true;

    public static Intent startIntent(Context context, int resultCode, Intent data) {
        Intent intent = new Intent(context, MediaProjectionService.class);
        intent.putExtra("RESULT_CODE", resultCode);
        intent.putExtra("DATA", data);
        return intent;
    }

    public static Intent stopIntent(Context context) {
        if (handler != null) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (mediaProjection != null) {
                        mediaProjection.stop();
                    }
                }
            });
        }

        Intent intent = new Intent(context, MediaProjectionService.class);
        intent.setAction("STOP_PROJECTION");
        return intent;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();

        new Thread(() -> {
            Looper.prepare();
            handler = new Handler();
            Looper.loop();
        }).start();
    }

    private static boolean isStartCommand(Intent intent) {
        return intent.hasExtra("RESULT_CODE") && intent.hasExtra("DATA");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (isStartCommand(intent)) {
            serverIp = intent.getStringExtra("server_ip");
            Log.i(TAG, "serverIp: " + serverIp);
            int resultCode = intent.getIntExtra("RESULT_CODE", Activity.RESULT_CANCELED);
            Intent data = intent.getParcelableExtra("DATA");

            startForegroundService();
            startProjection(resultCode, data);
        }

        if ("STOP_PROJECTION".equals(intent.getAction())) {
            stopProjection();
            return START_NOT_STICKY;
        }
        return START_NOT_STICKY;
    }

    private void startForegroundService() {
        Notification notification = createNotification();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
        } else {
            startForeground(1, notification);
        }
    }

    private Notification createNotification() {
        return new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("Screen Projection")
                .setContentText("Screen projection is running")
                .setSmallIcon(R.drawable.ic_launcher_background)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .build();
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "Screen Projection Service",
                    NotificationManager.IMPORTANCE_HIGH
            );
            NotificationManager manager = getSystemService(NotificationManager.class);
            if (manager != null) {
                manager.createNotificationChannel(channel);
            }
        }
    }

    @SuppressLint("WrongConstant")
    private void startProjection(int resultCode, Intent data) {
        MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        mediaProjection = projectionManager.getMediaProjection(resultCode, data);

        width = getResources().getDisplayMetrics().widthPixels;
        height = getResources().getDisplayMetrics().heightPixels;

        imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
        mediaProjection.createVirtualDisplay(
                "ScreenCapture",
                width,
                height,
                getResources().getDisplayMetrics().densityDpi,
                0,
                imageReader.getSurface(),
                null,
                new Handler(Looper.getMainLooper()));

        imageReader.setOnImageAvailableListener(new ClientScreenImageAvailableListener(), new Handler(Looper.getMainLooper()));
}

    private void stopProjection() {
        if (mediaProjection != null) {
            mediaProjection.stop();
            mediaProjection = null;
        }
        stopForeground(true); // Remove notification
        stopSelf(); // Stop service
    }

    @SuppressLint("WrongConstant")
    private void startProjection(int resultCode, Intent data) {
        MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        mediaProjection = projectionManager.getMediaProjection(resultCode, data);

        width = getResources().getDisplayMetrics().widthPixels;
        height = getResources().getDisplayMetrics().heightPixels;

        imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
        mediaProjection.createVirtualDisplay(
                "ScreenCapture",
                width,
                height,
                getResources().getDisplayMetrics().densityDpi,
                0,
                imageReader.getSurface(),
                null,
                new Handler(Looper.getMainLooper()));

        imageReader.setOnImageAvailableListener(new ClientScreenImageAvailableListener(), new Handler(Looper.getMainLooper()));
    }

    private class ClientScreenImageAvailableListener implements ImageReader.OnImageAvailableListener {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.i(TAG, "Image available from ImageReader.");
            Image image = null;
            try {
                image = reader.acquireLatestImage();
                Image finalImage = image;
                sendImageToServer(finalImage);
                finalImage.close();
            } catch (Exception e) {
                Log.e(TAG, "Error processing image", e);
            }
        }

        private void sendImageToServer(Image image) {
            try (Socket socket = new Socket(serverIp, 5000);
                 ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())) {

                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);

                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
                byte[] bitmapBytes = baos.toByteArray();

                outputStream.writeObject(bitmapBytes);
                outputStream.flush();

                Log.i(TAG, "Image sent to server successfully.");

                bitmap.recycle();
            } catch (Exception e) {
                Log.e(TAG, "Failed to send image to server", e);
            }
//            finally {
//                image.close();
//            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

日志信息

2024-08-29 14:00:20.114 27196-27196 MediaProjectionService              com.r.screenmediaprojection   I  serverIp: 0.0.0.0 (my ip is printed here)
2024-08-29 14:00:20.121 27196-27196 CompatibilityChangeReporter         com.r.screenmediaprojection   D  Compat change id reported: 236825255; UID 10748; state: DISABLED
2024-08-29 14:00:20.121 27196-27196 BufferQueueConsumer                 com.r.screenmediaprojection   D  [](id:6a3c00000003,api:0,p:-1,c:27196) connect: controlledByApp=true
2024-08-29 14:00:20.121 27196-27196 CompatibilityChangeReporter         com.r.screenmediaprojection   D  Compat change id reported: 269849258; UID 10748; state: DISABLED
2024-08-29 14:00:20.163 27196-27196 ImeFocusController                  com.r.screenmediaprojection   V  onWindowFocus: DecorView@8ca0efc[MainActivity] softInputMode=STATE_UNSPECIFIED|ADJUST_PAN
2024-08-29 14:00:20.178 27196-27196 MediaProjectionService              com.r.screenmediaprojection   I  Image available from ImageReader.
2024-08-29 14:00:20.182 27196-28395 TrafficStats                        com.r.screenmediaprojection   D  tagSocket(142) with statsTag=0xffffffff, statsUid=-1
2024-08-29 14:00:20.187 27196-27196 MediaProjectionService              com.r.screenmediaprojection   I  Image available from ImageReader.
2024-08-29 14:00:20.190 27196-27196 ImageReader_JNI                     com.r.screenmediaprojection   W  Unable to acquire a buffer item, very likely client tried to acquire more than maxImages buffers
2024-08-29 14:00:20.191 27196-28396 TrafficStats                        com.r.screenmediaprojection   D  tagSocket(148) with statsTag=0xffffffff, statsUid=-1
2024-08-29 14:00:20.202 27196-27196 MediaProjectionService              com.r.screenmediaprojection   I  Image available from ImageReader.
2024-08-29 14:00:20.202 27196-27196 ImageReader_JNI                     com.r.screenmediaprojection   W  Unable to acquire a buffer item, very likely client tried to acquire more than maxImages buffers
2024-08-29 14:00:20.203 27196-27196 MediaProjectionService              com.r.screenmediaprojection   E  Error processing image
                                                                                                                java.lang.IllegalStateException: maxImages (2) has already been acquired, call #close before acquiring more.
                                                                                                                    at android.media.ImageReader.acquireNextImage(ImageReader.java:672)
                                                                                                                    at android.media.ImageReader.acquireLatestImage(ImageReader.java:552)
                                                                                                                    at com.r.screenmediaprojection.service.MediaProjectionService$ClientScreenImageAvailableListener.onImageAvailable(MediaProjectionService.java:153)
                                                                                                                    at android.media.ImageReader$1.run(ImageReader.java:958)
                                                                                                                    at android.os.Handler.handleCallback(Handler.java:1013)
                                                                                                                    at android.os.Handler.dispatchMessage(Handler.java:101)
                                                                                                                    at android.os.Looper.loopOnce(Looper.java:226)
                                                                                                                    at android.os.Looper.loop(Looper.java:328)
                                                                                                                    at android.app.ActivityThread.main(ActivityThread.java:9188)
                                                                                                                    at java.lang.reflect.Method.invoke(Native Method)
                                                                                                                    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:594)
                                                                                                                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1099)
2024-08-29 14:00:20.742 27196-28396 VCode/native                        com.r.screenmediaprojection   D  track_event send
2024-08-29 14:00:20.743 27196-28396 OpenGLRenderer                      com.r.screenmediaprojection   D  Bitmap::compress format:Png w:1088 h:2204 quality:100 app_apk:com.r.screenmediaprojection 
2024-08-29 14:00:20.744 27196-28396 libc                                com.r.screenmediaprojection   W  Access denied finding property "vendor.vivo.imageencoder.dump"
2024-08-29 14:00:20.746 27196-28395 VCode/native                        com.r.screenmediaprojection   D  track_event send
2024-08-29 14:00:20.746 27196-28395 OpenGLRenderer                      com.r.screenmediaprojection   D  Bitmap::compress format:Png w:1088 h:2204 quality:100 app_apk:com.r.screenmediaprojection 
2024-08-29 14:00:20.747 27196-28395 libc                                com.r.screenmediaprojection   W  Access denied finding property "vendor.vivo.imageencoder.dump"
2024-08-29 14:00:20.966 27196-28395 MediaProjectionService              com.r.screenmediaprojection   I  Image sent to server successfully.
2024-08-29 14:00:20.967 27196-28395 MediaProjectionService              com.r.screenmediaprojection   D  Image closed after processing
2024-08-29 14:00:20.973 27196-28396 MediaProjectionService              com.r.screenmediaprojection   I  Image sent to server successfully.
2024-08-29 14:00:20.974 27196-28396 MediaProjectionService              com.r.screenmediaprojection   D  Image closed after processing

问题分析

报错的核心原因是主线程被耗时的网络发送操作阻塞,导致Image对象无法及时调用close()释放缓冲区:

  • onImageAvailable回调运行在主线程,你直接在该回调中同步执行sendImageToServer(包含TCP连接、Bitmap压缩、数据发送等耗时操作),导致主线程被阻塞。
  • 屏幕刷新是持续的,onImageAvailable会被频繁触发,在前面的Image还未完成发送、未调用close()释放缓冲区时,新的回调又获取了新的Image,很快就耗尽了ImageReader设置的2个缓冲区配额,触发溢出报错。

解决方案

1. 用线程池异步处理图像发送

将耗时的网络操作放到独立的线程池中执行,避免阻塞主线程,让Image能及时被close()释放缓冲区。

2. 确保Image在数据读取完成后再关闭

将Image的close()操作放到图像数据处理完成后的finally块中,防止资源泄漏。

修改后的关键代码

private class ClientScreenImageAvailableListener implements ImageReader.OnImageAvailableListener {
    // 创建固定大小的线程池处理图像发送,大小匹配maxImages
    private ExecutorService executor = Executors.newFixedThreadPool(2);

    @Override
    public void onImageAvailable(ImageReader reader) {
        Log.i(TAG, "Image available from ImageReader.");
        Image image = null;
        try {
            image = reader.acquireLatestImage();
            if (image == null) {
                return;
            }
            // 提交到线程池异步处理,避免阻塞主线程
            executor.submit(() -> processAndSendImage(image));
        } catch (Exception e) {
            Log.e(TAG, "Error acquiring image", e);
            // 获取失败时立即关闭Image
            if (image != null) {
                image.close();
            }
        }
    }

    private void processAndSendImage(Image image) {
        try (Socket
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 23:53:01