解决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
相关产品推荐
相关产品推荐

