Skip to content

Commit 0e812e9

Browse files
johnmccutchanreidbaker
authored andcommitted
Workaround HardwareRenderer breakage in Android 14 (flutter#52370)
- Destroy ImageReaders on memory trim. - Unset the VirtualDisplay's surface on memory trim. - On resume, recreate ImageReaders. - On resume, do a dumb little dance and then set the VirtualDisplay's surface Fixes: flutter/flutter#146499 Fixes: flutter/flutter#144219 Internal bug: b/335646931 Android Fix: https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/27015418 Android 15 will include the fix. Unclear if Android 14 will be patched.
1 parent c4cd48e commit 0e812e9

File tree

5 files changed

+424
-9
lines changed

5 files changed

+424
-9
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ void onPostResume() {
578578
ensureAlive();
579579
if (flutterEngine != null) {
580580
updateSystemUiOverlays();
581+
flutterEngine.getPlatformViewsController().onResume();
581582
} else {
582583
Log.w(TAG, "onPostResume() invoked before FlutterFragment was attached to an Activity.");
583584
}
@@ -921,6 +922,7 @@ void onTrimMemory(int level) {
921922
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
922923
}
923924
flutterEngine.getRenderer().onTrimMemory(level);
925+
flutterEngine.getPlatformViewsController().onTrimMemory(level);
924926
}
925927
}
926928

shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java

Lines changed: 233 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.flutter.embedding.engine.renderer;
66

77
import android.annotation.TargetApi;
8+
import android.content.ComponentCallbacks2;
89
import android.graphics.Bitmap;
910
import android.graphics.ImageFormat;
1011
import android.graphics.Rect;
@@ -361,17 +362,47 @@ public void run() {
361362
final class ImageReaderSurfaceProducer
362363
implements TextureRegistry.SurfaceProducer, TextureRegistry.ImageConsumer {
363364
private static final String TAG = "ImageReaderSurfaceProducer";
364-
private static final int MAX_IMAGES = 4;
365+
private static final int MAX_IMAGES = 5;
366+
367+
// Flip when debugging to see verbose logs.
368+
private static final boolean VERBOSE_LOGS = false;
369+
370+
// We must always cleanup on memory pressure on Android 14 due to a bug in Android.
371+
// It is safe to do on all versions so we unconditionally have this set to true.
372+
private static final boolean CLEANUP_ON_MEMORY_PRESSURE = true;
365373

366374
private final long id;
367375

368376
private boolean released;
369377
private boolean ignoringFence = false;
370378

371-
private int requestedWidth = 0;
372-
private int requestedHeight = 0;
373-
374379
/** Internal class: state held per image produced by image readers. */
380+
private boolean trimOnMemoryPressure = CLEANUP_ON_MEMORY_PRESSURE;
381+
382+
// The requested width and height are updated by setSize.
383+
private int requestedWidth = 1;
384+
private int requestedHeight = 1;
385+
// Whenever the requested width and height change we set this to be true so we
386+
// create a new ImageReader (inside getSurface) with the correct width and height.
387+
// We use this flag so that we lazily create the ImageReader only when a frame
388+
// will be produced at that size.
389+
private boolean createNewReader = true;
390+
391+
// State held to track latency of various stages.
392+
private long lastDequeueTime = 0;
393+
private long lastQueueTime = 0;
394+
private long lastScheduleTime = 0;
395+
private int numTrims = 0;
396+
397+
private Object lock = new Object();
398+
// REQUIRED: The following fields must only be accessed when lock is held.
399+
private final ArrayDeque<PerImageReader> imageReaderQueue = new ArrayDeque<PerImageReader>();
400+
private final HashMap<ImageReader, PerImageReader> perImageReaders =
401+
new HashMap<ImageReader, PerImageReader>();
402+
private PerImage lastDequeuedImage = null;
403+
private PerImageReader lastReaderDequeuedFrom = null;
404+
405+
/** Internal class: state held per Image produced by ImageReaders. */
375406
private class PerImage {
376407
public final ImageReader reader;
377408
public final Image image;
@@ -414,9 +445,184 @@ public void onImageAvailable(ImageReader reader) {
414445
return;
415446
}
416447
onImage(new PerImage(reader, image));
417-
}
448+
};
418449
};
419450

451+
PerImage dequeueImage() {
452+
if (imageQueue.size() == 0) {
453+
return null;
454+
}
455+
PerImage r = imageQueue.removeFirst();
456+
return r;
457+
}
458+
459+
/** returns true if we can prune this reader */
460+
boolean canPrune() {
461+
return imageQueue.size() == 0 && lastReaderDequeuedFrom != this;
462+
}
463+
464+
void close() {
465+
closed = true;
466+
if (VERBOSE_LOGS) {
467+
Log.i(TAG, "Closing reader=" + reader.hashCode());
468+
}
469+
reader.close();
470+
imageQueue.clear();
471+
}
472+
473+
double deltaMillis(long deltaNanos) {
474+
double ms = (double) deltaNanos / (double) 1000000.0;
475+
return ms;
476+
}
477+
478+
PerImageReader getOrCreatePerImageReader(ImageReader reader) {
479+
PerImageReader r = perImageReaders.get(reader);
480+
if (r == null) {
481+
r = new PerImageReader(reader);
482+
perImageReaders.put(reader, r);
483+
imageReaderQueue.add(r);
484+
if (VERBOSE_LOGS) {
485+
Log.i(TAG, "imageReaderQueue#=" + imageReaderQueue.size());
486+
}
487+
}
488+
return r;
489+
}
490+
491+
void pruneImageReaderQueue() {
492+
boolean change = false;
493+
// Prune nodes from the head of the ImageReader queue.
494+
while (imageReaderQueue.size() > 1) {
495+
PerImageReader r = imageReaderQueue.peekFirst();
496+
if (!r.canPrune()) {
497+
// No more ImageReaders can be pruned this round.
498+
break;
499+
}
500+
imageReaderQueue.removeFirst();
501+
perImageReaders.remove(r.reader);
502+
r.close();
503+
change = true;
504+
}
505+
if (change && VERBOSE_LOGS) {
506+
Log.i(TAG, "Pruned image reader queue length=" + imageReaderQueue.size());
507+
}
508+
}
509+
510+
void onImage(ImageReader reader, Image image) {
511+
PerImage queuedImage = null;
512+
synchronized (lock) {
513+
PerImageReader perReader = getOrCreatePerImageReader(reader);
514+
queuedImage = perReader.queueImage(image);
515+
}
516+
if (queuedImage == null) {
517+
// We got a late image.
518+
return;
519+
}
520+
if (VERBOSE_LOGS) {
521+
if (lastQueueTime != 0) {
522+
long now = System.nanoTime();
523+
long queueDelta = now - lastQueueTime;
524+
Log.i(
525+
TAG,
526+
""
527+
+ reader.hashCode()
528+
+ " enqueued image="
529+
+ queuedImage.image.hashCode()
530+
+ " queueDelta="
531+
+ deltaMillis(queueDelta));
532+
lastQueueTime = now;
533+
} else {
534+
lastQueueTime = System.nanoTime();
535+
}
536+
}
537+
scheduleEngineFrame();
538+
}
539+
540+
public PerImageReader(ImageReader reader) {
541+
this.reader = reader;
542+
reader.setOnImageAvailableListener(
543+
onImageAvailableListener, new Handler(Looper.getMainLooper()));
544+
}
545+
546+
PerImage queueImage(Image image) {
547+
if (closed) {
548+
return null;
549+
}
550+
PerImage perImage = new PerImage(image, System.nanoTime());
551+
imageQueue.add(perImage);
552+
// If we fall too far behind we will skip some frames.
553+
while (imageQueue.size() > 2) {
554+
PerImage r = imageQueue.removeFirst();
555+
if (VERBOSE_LOGS) {
556+
Log.i(TAG, "" + reader.hashCode() + " force closed image=" + r.image.hashCode());
557+
}
558+
r.image.close();
559+
}
560+
return perImage;
561+
}
562+
563+
@Override
564+
public void onTrimMemory(int level) {
565+
if (!trimOnMemoryPressure) {
566+
return;
567+
}
568+
if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
569+
return;
570+
}
571+
synchronized (lock) {
572+
numTrims++;
573+
}
574+
cleanup();
575+
createNewReader = true;
576+
}
577+
578+
private void cleanup() {
579+
synchronized (lock) {
580+
for (PerImageReader pir : perImageReaders.values()) {
581+
if (lastReaderDequeuedFrom == pir) {
582+
lastReaderDequeuedFrom = null;
583+
}
584+
pir.close();
585+
}
586+
perImageReaders.clear();
587+
if (lastDequeuedImage != null) {
588+
lastDequeuedImage.image.close();
589+
lastDequeuedImage = null;
590+
}
591+
if (lastReaderDequeuedFrom != null) {
592+
lastReaderDequeuedFrom.close();
593+
lastReaderDequeuedFrom = null;
594+
}
595+
imageReaderQueue.clear();
596+
}
597+
}
598+
599+
@TargetApi(API_LEVELS.API_33)
600+
private void waitOnFence(Image image) {
601+
try {
602+
SyncFence fence = image.getFence();
603+
fence.awaitForever();
604+
} catch (IOException e) {
605+
// Drop.
606+
}
607+
}
608+
609+
private void maybeWaitOnFence(Image image) {
610+
if (image == null) {
611+
return;
612+
}
613+
if (ignoringFence) {
614+
return;
615+
}
616+
if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
617+
// The fence API is only available on Android >= 33.
618+
waitOnFence(image);
619+
return;
620+
}
621+
// Log once per ImageTextureEntry.
622+
ignoringFence = true;
623+
Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33");
624+
}
625+
420626
ImageReaderSurfaceProducer(long id) {
421627
this.id = id;
422628
}
@@ -662,8 +868,28 @@ public void disableFenceForTest() {
662868
}
663869

664870
@VisibleForTesting
665-
public int readersToCloseSize() {
666-
return readersToClose.size();
871+
public int numImageReaders() {
872+
synchronized (lock) {
873+
return imageReaderQueue.size();
874+
}
875+
}
876+
877+
@VisibleForTesting
878+
public int numTrims() {
879+
synchronized (lock) {
880+
return numTrims;
881+
}
882+
}
883+
884+
@VisibleForTesting
885+
public int numImages() {
886+
int r = 0;
887+
synchronized (lock) {
888+
for (PerImageReader reader : imageReaderQueue) {
889+
r += reader.imageQueue.size();
890+
}
891+
}
892+
return r;
667893
}
668894
}
669895

shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static android.view.MotionEvent.PointerProperties;
99

1010
import android.annotation.TargetApi;
11+
import android.content.ComponentCallbacks2;
1112
import android.content.Context;
1213
import android.content.MutableContextWrapper;
1314
import android.os.Build;
@@ -1056,6 +1057,24 @@ private void diposeAllViews() {
10561057
}
10571058
}
10581059

1060+
// Invoked when the Android system is requesting we reduce memory usage.
1061+
public void onTrimMemory(int level) {
1062+
if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
1063+
return;
1064+
}
1065+
for (VirtualDisplayController vdc : vdControllers.values()) {
1066+
vdc.clearSurface();
1067+
}
1068+
}
1069+
1070+
// Called after the application has been resumed.
1071+
// This is where we undo whatever may have been done in onTrimMemory.
1072+
public void onResume() {
1073+
for (VirtualDisplayController vdc : vdControllers.values()) {
1074+
vdc.resetSurface();
1075+
}
1076+
}
1077+
10591078
/**
10601079
* Disposes a single
10611080
*

shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,49 @@ public void dispatchTouchEvent(MotionEvent event) {
261261
presentation.dispatchTouchEvent(event);
262262
}
263263

264+
public void clearSurface() {
265+
virtualDisplay.setSurface(null);
266+
}
267+
268+
public void resetSurface() {
269+
final int width = getRenderTargetWidth();
270+
final int height = getRenderTargetHeight();
271+
final boolean isFocused = getView().isFocused();
272+
final SingleViewPresentation.PresentationState presentationState = presentation.detachState();
273+
274+
// We detach the surface to prevent it being destroyed when releasing the vd.
275+
virtualDisplay.setSurface(null);
276+
virtualDisplay.release();
277+
final DisplayManager displayManager =
278+
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
279+
int flags = 0;
280+
virtualDisplay =
281+
displayManager.createVirtualDisplay(
282+
"flutter-vd#" + viewId,
283+
width,
284+
height,
285+
densityDpi,
286+
renderTarget.getSurface(),
287+
flags,
288+
callback,
289+
null /* handler */);
290+
// Create a new SingleViewPresentation and show() it before we cancel() the existing
291+
// presentation. Calling show() and cancel() in this order fixes
292+
// https://github.com/flutter/flutter/issues/26345 and maintains seamless transition
293+
// of the contents of the presentation.
294+
SingleViewPresentation newPresentation =
295+
new SingleViewPresentation(
296+
context,
297+
virtualDisplay.getDisplay(),
298+
accessibilityEventsDelegate,
299+
presentationState,
300+
focusChangeListener,
301+
isFocused);
302+
newPresentation.show();
303+
presentation.cancel();
304+
presentation = newPresentation;
305+
}
306+
264307
static class OneTimeOnDrawListener implements ViewTreeObserver.OnDrawListener {
265308
static void schedule(View view, Runnable runnable) {
266309
OneTimeOnDrawListener listener = new OneTimeOnDrawListener(view, runnable);

0 commit comments

Comments
 (0)