Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Fix ImageReader may leak images when onDraw() not called. #23909

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.RenderSurface;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;

/**
* Paints a Flutter UI provided by an {@link android.media.ImageReader} onto a {@link
Expand All @@ -41,7 +38,6 @@
@TargetApi(19)
public class FlutterImageView extends View implements RenderSurface {
@NonNull private ImageReader imageReader;
@Nullable private Queue<Image> imageQueue;
@Nullable private Image currentImage;
@Nullable private Bitmap currentBitmap;
@Nullable private FlutterRenderer flutterRenderer;
Expand All @@ -57,13 +53,6 @@ public enum SurfaceKind {
/** The kind of surface. */
private SurfaceKind kind;

/**
* The number of images acquired from the current {@link android.media.ImageReader} that are
* waiting to be painted. This counter is decreased after calling {@link
* android.media.Image#close()}.
*/
private int pendingImages = 0;

/** Whether the view is attached to the Flutter render. */
private boolean isAttachedToFlutterRenderer = false;

Expand All @@ -89,7 +78,6 @@ public FlutterImageView(@NonNull Context context, @NonNull AttributeSet attrs) {
super(context, null);
this.imageReader = imageReader;
this.kind = kind;
this.imageQueue = new LinkedList<>();
init();
}

Expand Down Expand Up @@ -155,22 +143,14 @@ public void detachFromRenderer() {
return;
}
setAlpha(0.0f);
// Drop the lastest image as it shouldn't render this image if this view is
// Drop the latest image as it shouldn't render this image if this view is
// attached to the renderer again.
acquireLatestImage();
// Clear drawings.
currentBitmap = null;

// Close the images in the queue and clear the queue.
for (final Image image : imageQueue) {
image.close();
}
imageQueue.clear();
// Close and clear the current image if any.
if (currentImage != null) {
currentImage.close();
currentImage = null;
}
closeCurrentImage();
invalidate();
isAttachedToFlutterRenderer = false;
}
Expand All @@ -188,26 +168,22 @@ public boolean acquireLatestImage() {
if (!isAttachedToFlutterRenderer) {
return false;
}
// There's no guarantee that the image will be closed before the next call to
// `acquireLatestImage()`. For example, the device may not produce new frames if
// it's in sleep mode, so the calls to `invalidate()` will be queued up
// until the device produces a new frame.
// 1. `acquireLatestImage()` may return null if no new image is available.
//
// While the engine will also stop producing frames, there is a race condition.
// 2. There's no guarantee that the `onDraw()` called after `invalidate()`.
// For example, the device may not produce new frames if
// it's in sleep mode or some special Android devices so the calls to `invalidate()` will be queued up
// until the device produces a new frame.
//
// To avoid exceptions, check if a new image can be acquired.
int imageOpenedCount = imageQueue.size();
if (currentImage != null) {
imageOpenedCount++;
}
if (imageOpenedCount < imageReader.getMaxImages()) {
final Image image = imageReader.acquireLatestImage();
if (image != null) {
imageQueue.add(image);
}
// 3. While the engine will also stop producing frames, there is a race condition.
final Image newImage = imageReader.acquireLatestImage();
if (newImage != null) {
// Only close current image after acquiring valid new image
closeCurrentImage();
currentImage = newImage;
invalidate();
}
invalidate();
return !imageQueue.isEmpty();
return newImage != null;
}

/** Creates a new image reader with the provided size. */
Expand All @@ -218,32 +194,36 @@ public void resizeIfNeeded(int width, int height) {
if (width == imageReader.getWidth() && height == imageReader.getHeight()) {
return;
}
imageQueue.clear();
currentImage = null;

// Close resources.
closeCurrentImage();

// Close all the resources associated with the image reader,
// including the images.
imageReader.close();
// Image readers cannot be resized once created.
imageReader = createImageReader(width, height);
pendingImages = 0;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (!imageQueue.isEmpty()) {
if (currentImage != null) {
currentImage.close();
}
currentImage = imageQueue.poll();
if (currentImage != null) {
updateCurrentBitmap();
}
if (currentBitmap != null) {
canvas.drawBitmap(currentBitmap, 0, 0, null);
}
}

private void closeCurrentImage() {
// Close and clear the current image if any.
if (currentImage != null) {
currentImage.close();
currentImage = null;
}
}

@TargetApi(29)
private void updateCurrentBitmap() {
if (android.os.Build.VERSION.SDK_INT >= 29) {
Expand Down