-
Notifications
You must be signed in to change notification settings - Fork 6k
Fix ImageReader may leak images when onDraw() not called #24272
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,8 +23,6 @@ | |
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 | ||
|
@@ -41,7 +39,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; | ||
|
@@ -57,13 +54,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; | ||
|
||
|
@@ -89,7 +79,6 @@ public FlutterImageView(@NonNull Context context, @NonNull AttributeSet attrs) { | |
super(context, null); | ||
this.imageReader = imageReader; | ||
this.kind = kind; | ||
this.imageQueue = new LinkedList<>(); | ||
init(); | ||
} | ||
|
||
|
@@ -155,22 +144,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; | ||
} | ||
|
@@ -188,26 +169,20 @@ 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 | ||
// 1. `acquireLatestImage()` may return null if no new image is available. | ||
// 2. There's no guarantee that `onDraw()` is 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()` queued up | ||
// until the device produces a new frame. | ||
// | ||
// While the engine will also stop producing frames, there is a race condition. | ||
// | ||
// To avoid exceptions, check if a new image can be acquired. | ||
int imageOpenedCount = imageQueue.size(); | ||
if (currentImage != null) { | ||
imageOpenedCount++; | ||
// 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(); | ||
} | ||
if (imageOpenedCount < imageReader.getMaxImages()) { | ||
final Image image = imageReader.acquireLatestImage(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like there is some misunderstanding from my side. The error message " Unable to acquire a buffer item, very likely client tried to acquire more than maxImages buffers" suggests that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @blasten I guess maybe the newest available image will be also counted by the code in ImageReader.cpp. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @blasten I read the code and document of The doc of
So it needs at least two reserved slots in buffer before one calling of public Image acquireLatestImage() {
Image image = acquireNextImage();
if (image == null) {
return null;
}
try {
for (;;) {
Image next = acquireNextImageNoThrowISE();
if (next == null) {
Image result = image;
image = null;
return result;
}
image.close();
image = next;
}
} finally {
if (image != null) {
image.close();
}
}
} Every The code of this pull request fixs onDraw() may be lesser then invalidate(), and try to make the code logic neat and easy to maintain by closing the previous image if exists after |
||
if (image != null) { | ||
imageQueue.add(image); | ||
} | ||
} | ||
invalidate(); | ||
return !imageQueue.isEmpty(); | ||
return newImage != null; | ||
} | ||
|
||
/** Creates a new image reader with the provided size. */ | ||
|
@@ -218,32 +193,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) { | ||
|
Uh oh!
There was an error while loading. Please reload this page.