Skip to content

Commit e9e738c

Browse files
authored
Fix ImageReader may leak images when onDraw() not called (flutter#24272)
1 parent 625f453 commit e9e738c

File tree

2 files changed

+50
-136
lines changed

2 files changed

+50
-136
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterImageView.java

+27-48
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import io.flutter.embedding.engine.renderer.FlutterRenderer;
2424
import io.flutter.embedding.engine.renderer.RenderSurface;
2525
import java.nio.ByteBuffer;
26-
import java.util.LinkedList;
27-
import java.util.Queue;
2826

2927
/**
3028
* Paints a Flutter UI provided by an {@link android.media.ImageReader} onto a {@link
@@ -41,7 +39,6 @@
4139
@TargetApi(19)
4240
public class FlutterImageView extends View implements RenderSurface {
4341
@NonNull private ImageReader imageReader;
44-
@Nullable private Queue<Image> imageQueue;
4542
@Nullable private Image currentImage;
4643
@Nullable private Bitmap currentBitmap;
4744
@Nullable private FlutterRenderer flutterRenderer;
@@ -57,13 +54,6 @@ public enum SurfaceKind {
5754
/** The kind of surface. */
5855
private SurfaceKind kind;
5956

60-
/**
61-
* The number of images acquired from the current {@link android.media.ImageReader} that are
62-
* waiting to be painted. This counter is decreased after calling {@link
63-
* android.media.Image#close()}.
64-
*/
65-
private int pendingImages = 0;
66-
6757
/** Whether the view is attached to the Flutter render. */
6858
private boolean isAttachedToFlutterRenderer = false;
6959

@@ -89,7 +79,6 @@ public FlutterImageView(@NonNull Context context, @NonNull AttributeSet attrs) {
8979
super(context, null);
9080
this.imageReader = imageReader;
9181
this.kind = kind;
92-
this.imageQueue = new LinkedList<>();
9382
init();
9483
}
9584

@@ -155,22 +144,14 @@ public void detachFromRenderer() {
155144
return;
156145
}
157146
setAlpha(0.0f);
158-
// Drop the lastest image as it shouldn't render this image if this view is
147+
// Drop the latest image as it shouldn't render this image if this view is
159148
// attached to the renderer again.
160149
acquireLatestImage();
161150
// Clear drawings.
162151
currentBitmap = null;
163152

164-
// Close the images in the queue and clear the queue.
165-
for (final Image image : imageQueue) {
166-
image.close();
167-
}
168-
imageQueue.clear();
169153
// Close and clear the current image if any.
170-
if (currentImage != null) {
171-
currentImage.close();
172-
currentImage = null;
173-
}
154+
closeCurrentImage();
174155
invalidate();
175156
isAttachedToFlutterRenderer = false;
176157
}
@@ -188,26 +169,20 @@ public boolean acquireLatestImage() {
188169
if (!isAttachedToFlutterRenderer) {
189170
return false;
190171
}
191-
// There's no guarantee that the image will be closed before the next call to
192-
// `acquireLatestImage()`. For example, the device may not produce new frames if
193-
// it's in sleep mode, so the calls to `invalidate()` will be queued up
172+
// 1. `acquireLatestImage()` may return null if no new image is available.
173+
// 2. There's no guarantee that `onDraw()` is called after `invalidate()`.
174+
// For example, the device may not produce new frames if it's in sleep mode
175+
// or some special Android devices so the calls to `invalidate()` queued up
194176
// until the device produces a new frame.
195-
//
196-
// While the engine will also stop producing frames, there is a race condition.
197-
//
198-
// To avoid exceptions, check if a new image can be acquired.
199-
int imageOpenedCount = imageQueue.size();
200-
if (currentImage != null) {
201-
imageOpenedCount++;
177+
// 3. While the engine will also stop producing frames, there is a race condition.
178+
final Image newImage = imageReader.acquireLatestImage();
179+
if (newImage != null) {
180+
// Only close current image after acquiring valid new image
181+
closeCurrentImage();
182+
currentImage = newImage;
183+
invalidate();
202184
}
203-
if (imageOpenedCount < imageReader.getMaxImages()) {
204-
final Image image = imageReader.acquireLatestImage();
205-
if (image != null) {
206-
imageQueue.add(image);
207-
}
208-
}
209-
invalidate();
210-
return !imageQueue.isEmpty();
185+
return newImage != null;
211186
}
212187

213188
/** Creates a new image reader with the provided size. */
@@ -218,32 +193,36 @@ public void resizeIfNeeded(int width, int height) {
218193
if (width == imageReader.getWidth() && height == imageReader.getHeight()) {
219194
return;
220195
}
221-
imageQueue.clear();
222-
currentImage = null;
196+
197+
// Close resources.
198+
closeCurrentImage();
199+
223200
// Close all the resources associated with the image reader,
224201
// including the images.
225202
imageReader.close();
226203
// Image readers cannot be resized once created.
227204
imageReader = createImageReader(width, height);
228-
pendingImages = 0;
229205
}
230206

231207
@Override
232208
protected void onDraw(Canvas canvas) {
233209
super.onDraw(canvas);
234-
235-
if (!imageQueue.isEmpty()) {
236-
if (currentImage != null) {
237-
currentImage.close();
238-
}
239-
currentImage = imageQueue.poll();
210+
if (currentImage != null) {
240211
updateCurrentBitmap();
241212
}
242213
if (currentBitmap != null) {
243214
canvas.drawBitmap(currentBitmap, 0, 0, null);
244215
}
245216
}
246217

218+
private void closeCurrentImage() {
219+
// Close and clear the current image if any.
220+
if (currentImage != null) {
221+
currentImage.close();
222+
currentImage = null;
223+
}
224+
}
225+
247226
@TargetApi(29)
248227
private void updateCurrentBitmap() {
249228
if (android.os.Build.VERSION.SDK_INT >= 29) {

shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java

+23-88
Original file line numberDiff line numberDiff line change
@@ -658,36 +658,19 @@ public void flutterImageView_acquiresImageAndInvalidates() {
658658
verify(imageView, times(1)).invalidate();
659659
}
660660

661-
@Test
662-
public void flutterImageView_acquireLatestImageReturnsFalse() {
663-
final ImageReader mockReader = mock(ImageReader.class);
664-
when(mockReader.getMaxImages()).thenReturn(2);
665-
666-
final FlutterImageView imageView =
667-
spy(
668-
new FlutterImageView(
669-
RuntimeEnvironment.application,
670-
mockReader,
671-
FlutterImageView.SurfaceKind.background));
672-
673-
assertFalse(imageView.acquireLatestImage());
674-
675-
final FlutterJNI jni = mock(FlutterJNI.class);
676-
imageView.attachToRenderer(new FlutterRenderer(jni));
677-
678-
when(mockReader.acquireLatestImage()).thenReturn(null);
679-
assertFalse(imageView.acquireLatestImage());
680-
}
681-
682661
@Test
683662
@SuppressLint("WrongCall") /*View#onDraw*/
684-
public void flutterImageView_acquiresMaxImagesAtMost() {
663+
public void flutterImageView_acquiresImageClosesPreviousImageUnlessNoNewImage() {
685664
final ImageReader mockReader = mock(ImageReader.class);
686665
when(mockReader.getMaxImages()).thenReturn(3);
687666

688667
final Image mockImage = mock(Image.class);
689668
when(mockImage.getPlanes()).thenReturn(new Plane[0]);
690-
when(mockReader.acquireLatestImage()).thenReturn(mockImage);
669+
// Mock no latest image on the second time
670+
when(mockReader.acquireLatestImage())
671+
.thenReturn(mockImage)
672+
.thenReturn(null)
673+
.thenReturn(mockImage);
691674

692675
final FlutterImageView imageView =
693676
spy(
@@ -700,33 +683,27 @@ public void flutterImageView_acquiresMaxImagesAtMost() {
700683
imageView.attachToRenderer(new FlutterRenderer(jni));
701684
doNothing().when(imageView).invalidate();
702685

703-
assertTrue(imageView.acquireLatestImage()); // 1 image
704-
assertTrue(imageView.acquireLatestImage()); // 2 images
705-
assertTrue(imageView.acquireLatestImage()); // 3 images
706-
assertTrue(imageView.acquireLatestImage()); // 3 images
707-
verify(mockReader, times(3)).acquireLatestImage();
686+
assertTrue(imageView.acquireLatestImage()); // No previous, acquire latest image
687+
assertFalse(
688+
imageView.acquireLatestImage()); // Mock no image when acquire, don't close, and assertFalse
689+
assertTrue(imageView.acquireLatestImage()); // Acquire latest image and close previous
690+
assertTrue(imageView.acquireLatestImage()); // Acquire latest image and close previous
691+
assertTrue(imageView.acquireLatestImage()); // Acquire latest image and close previous
692+
verify(mockImage, times(3)).close(); // Close 3 times
708693

709-
imageView.onDraw(mock(Canvas.class)); // 3 images
710-
assertTrue(imageView.acquireLatestImage()); // 3 images
711-
verify(mockReader, times(3)).acquireLatestImage();
694+
imageView.onDraw(mock(Canvas.class)); // Draw latest image
712695

713-
imageView.onDraw(mock(Canvas.class)); // 2 images
714-
assertTrue(imageView.acquireLatestImage()); // 3 images
715-
verify(mockReader, times(4)).acquireLatestImage();
696+
assertTrue(imageView.acquireLatestImage()); // acquire latest image and close previous
716697

717-
imageView.onDraw(mock(Canvas.class)); // 2 images
718-
imageView.onDraw(mock(Canvas.class)); // 1 image
719-
imageView.onDraw(mock(Canvas.class)); // 1 image
698+
imageView.onDraw(mock(Canvas.class)); // Draw latest image
699+
imageView.onDraw(mock(Canvas.class)); // Draw latest image
700+
imageView.onDraw(mock(Canvas.class)); // Draw latest image
720701

721-
assertTrue(imageView.acquireLatestImage()); // 2 images
722-
assertTrue(imageView.acquireLatestImage()); // 3 images
723-
assertTrue(imageView.acquireLatestImage()); // 3 images
724-
assertTrue(imageView.acquireLatestImage()); // 3 images
725702
verify(mockReader, times(6)).acquireLatestImage();
726703
}
727704

728705
@Test
729-
public void flutterImageView_detachFromRendererClosesAllImages() {
706+
public void flutterImageView_detachFromRendererClosesPreviousImage() {
730707
final ImageReader mockReader = mock(ImageReader.class);
731708
when(mockReader.getMaxImages()).thenReturn(2);
732709

@@ -746,54 +723,12 @@ public void flutterImageView_detachFromRendererClosesAllImages() {
746723
doNothing().when(imageView).invalidate();
747724
imageView.acquireLatestImage();
748725
imageView.acquireLatestImage();
749-
imageView.detachFromRenderer();
750-
751-
verify(mockImage, times(2)).close();
752-
}
753-
754-
@Test
755-
@SuppressLint("WrongCall") /*View#onDraw*/
756-
public void flutterImageView_onDrawClosesAllImages() {
757-
final ImageReader mockReader = mock(ImageReader.class);
758-
when(mockReader.getMaxImages()).thenReturn(2);
759-
760-
final Image mockImage = mock(Image.class);
761-
when(mockImage.getPlanes()).thenReturn(new Plane[0]);
762-
when(mockReader.acquireLatestImage()).thenReturn(mockImage);
763-
764-
final FlutterImageView imageView =
765-
spy(
766-
new FlutterImageView(
767-
RuntimeEnvironment.application,
768-
mockReader,
769-
FlutterImageView.SurfaceKind.background));
770-
771-
final FlutterJNI jni = mock(FlutterJNI.class);
772-
imageView.attachToRenderer(new FlutterRenderer(jni));
773-
774-
doNothing().when(imageView).invalidate();
775-
imageView.acquireLatestImage();
776-
imageView.acquireLatestImage();
777-
778-
imageView.onDraw(mock(Canvas.class));
779-
imageView.onDraw(mock(Canvas.class));
780-
781-
// 1 image is closed and 1 is active.
782-
verify(mockImage, times(1)).close();
783-
verify(mockReader, times(2)).acquireLatestImage();
784-
785-
// This call doesn't do anything because there isn't
786-
// an image in the queue.
787-
imageView.onDraw(mock(Canvas.class));
788726
verify(mockImage, times(1)).close();
789727

790-
// Aquire another image and push it to the queue.
791-
imageView.acquireLatestImage();
792-
verify(mockReader, times(3)).acquireLatestImage();
793-
794-
// Then, the second image is closed.
795-
imageView.onDraw(mock(Canvas.class));
796-
verify(mockImage, times(2)).close();
728+
imageView.detachFromRenderer();
729+
// There's an acquireLatestImage() in detachFromRenderer(),
730+
// so it will be 2 times called close() inside detachFromRenderer()
731+
verify(mockImage, times(3)).close();
797732
}
798733

799734
@Test

0 commit comments

Comments
 (0)