-
Notifications
You must be signed in to change notification settings - Fork 6k
Fix ImageReader may leak images when onDraw() not called #24272
Conversation
imageOpenedCount++; | ||
} | ||
if (imageOpenedCount < imageReader.getMaxImages()) { | ||
final Image image = imageReader.acquireLatestImage(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 imageReader.acquireLatestImage()
was called even though imageOpenedCount
is less than imageReader.getMaxImages()
. Unless there's a bug in this logic, shouldn't it be preventing the acquireLatestImage()
call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
Let me have a deeper view in ImageReader.cpp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blasten I read the code and document of acquireLatestImage()
in ImageReader.java carefully and I finally know why the error message comes even though the openedCount is less than maxImages.
The doc of acquireLatestImage()
says:
* Note that {@link #getMaxImages maxImages} should be at least 2 for
* {@link #acquireLatestImage} to be any different than {@link #acquireNextImage} -
* discarding all-but-the-newest {@link Image} requires temporarily acquiring two
* {@link Image Images} at once. Or more generally, calling {@link #acquireLatestImage}
* with less than two images of margin, that is
* {@code (maxImages - currentAcquiredImages < 2)} will not discard as expected.
* </p>
So it needs at least two reserved slots in buffer before one calling of acquireLatestImage()
. And the implementation code is as same as it described:
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 nativeImageSetup()
will need a new BufferItem in cpp, acquireNextSurfaceImage()
calls nativeImageSetup()
. then looking at the acquireLatestImage()
logic, it calls acquireNextImage()
and calls acquireNextImageNoThrowISE
in the for-loop and close the last image unless the next is null. So It can really need two slots.
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 reader.acquireLatestImages()
and removing the queue and the counter.
shell/platform/android/io/flutter/embedding/android/FlutterImageView.java
Show resolved
Hide resolved
shell/platform/android/io/flutter/embedding/android/FlutterImageView.java
Outdated
Show resolved
Hide resolved
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would you mind trying to justify the comments, so the line width is a bit more even?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blasten OK, I will justify the comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blasten Done~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great contribution @eggfly. Awesome work sharing your findings. LGTM
It fixes: flutter/flutter#63897 flutter/flutter#70290 The onDraw() may not called after invalidate(), this maybe reproduced on some Android devices. And also ImageReader related logics can be simplified.
@liyuqian @chinmaygarde Would you like to take a moment to have a review on this PR? Thank u 😄 |
@eggfly it will get merged once the tree becomes green |
Thanks @eggfly . Was waiting for this pr. |
Description
The onDraw() may not called after invalidate(), it makes some flash or flickering of flutter elements. This can be reproduced on some Android devices. (For example: HUAWEI P30 or HUAWEI Mate 30 PRO.)
And as the screen recording below:
Screenrecorder-2021-02-07-18-19-48-960.mp4
I made a demo code in main.dart:
and using webview_flutter 1.0.7 version in pubspec.yaml:
And also there's a log: "unable to acquire a buffer item, very likely client tried to acquire more than maximages buffers".
The imageQueue in old implementation may leaves two or more items after acquireLatestImage() and occure the native level log:
"unable to acquire a buffer item, very likely client tried to acquire more than maximages buffers" in https://android.googlesource.com/platform/frameworks/base/+/master/media/jni/android_media_ImageReader.cpp#517
ROOT CAUSE
Because there's no guarantee that onDraw() must be called after invalidate() when device sleeping or on some special devices.
Not all devices are reproduced. So we can add a modification to the flutter engine to simulate this problem on most Android phones.
shell/platform/android/io/flutter/embedding/android/FlutterImageView.java
:This can simulate the missing call of onDraw() once every 10 times. And the flashing or widgets can be observed as well as video above.
Solution
So after all, I made this change and I also think ImageReader related logics can be simplified:
imageReader.acquireLatestImage()
returns a new image, and no need to record a queue and no need to checkimageReader.getMaxImages()
It fixes:
flutter/flutter#63897
flutter/flutter#70290
Test
The test code:
flutterImageView_onDrawClosesAllImages()
with old logic.acquiresMaxImagesAtMost
andacquireLatestImageReturnsFalse()
to the testacquiresImageClosesPreviousImageUnlessNoNewImage
, it can covermockReader.acquireLatestImage()
returns null making the method returns false when no latest image, and theonDraw()
missing cases.Pre-launch Checklist
writing and running engine tests.
///
).