Skip to content

Commit ec1b6ad

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 ec1b6ad

File tree

5 files changed

+468
-9
lines changed

5 files changed

+468
-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: 277 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;
@@ -415,7 +446,226 @@ public void onImageAvailable(ImageReader reader) {
415446
}
416447
onImage(new PerImage(reader, image));
417448
}
418-
};
449+
r.image.close();
450+
}
451+
return perImage;
452+
}
453+
454+
PerImage dequeueImage() {
455+
if (imageQueue.size() == 0) {
456+
return null;
457+
}
458+
PerImage r = imageQueue.removeFirst();
459+
return r;
460+
}
461+
462+
/** returns true if we can prune this reader */
463+
boolean canPrune() {
464+
return imageQueue.size() == 0 && lastReaderDequeuedFrom != this;
465+
}
466+
467+
void close() {
468+
closed = true;
469+
if (VERBOSE_LOGS) {
470+
Log.i(TAG, "Closing reader=" + reader.hashCode());
471+
}
472+
reader.close();
473+
imageQueue.clear();
474+
}
475+
}
476+
477+
double deltaMillis(long deltaNanos) {
478+
double ms = (double) deltaNanos / (double) 1000000.0;
479+
return ms;
480+
}
481+
482+
PerImageReader getOrCreatePerImageReader(ImageReader reader) {
483+
PerImageReader r = perImageReaders.get(reader);
484+
if (r == null) {
485+
r = new PerImageReader(reader);
486+
perImageReaders.put(reader, r);
487+
imageReaderQueue.add(r);
488+
if (VERBOSE_LOGS) {
489+
Log.i(TAG, "imageReaderQueue#=" + imageReaderQueue.size());
490+
}
491+
}
492+
return r;
493+
}
494+
495+
void pruneImageReaderQueue() {
496+
boolean change = false;
497+
// Prune nodes from the head of the ImageReader queue.
498+
while (imageReaderQueue.size() > 1) {
499+
PerImageReader r = imageReaderQueue.peekFirst();
500+
if (!r.canPrune()) {
501+
// No more ImageReaders can be pruned this round.
502+
break;
503+
}
504+
imageReaderQueue.removeFirst();
505+
perImageReaders.remove(r.reader);
506+
r.close();
507+
change = true;
508+
}
509+
if (change && VERBOSE_LOGS) {
510+
Log.i(TAG, "Pruned image reader queue length=" + imageReaderQueue.size());
511+
}
512+
}
513+
514+
void onImage(ImageReader reader, Image image) {
515+
PerImage queuedImage = null;
516+
synchronized (lock) {
517+
PerImageReader perReader = getOrCreatePerImageReader(reader);
518+
queuedImage = perReader.queueImage(image);
519+
}
520+
if (queuedImage == null) {
521+
// We got a late image.
522+
return;
523+
}
524+
if (VERBOSE_LOGS) {
525+
if (lastQueueTime != 0) {
526+
long now = System.nanoTime();
527+
long queueDelta = now - lastQueueTime;
528+
Log.i(
529+
TAG,
530+
""
531+
+ reader.hashCode()
532+
+ " enqueued image="
533+
+ queuedImage.image.hashCode()
534+
+ " queueDelta="
535+
+ deltaMillis(queueDelta));
536+
lastQueueTime = now;
537+
} else {
538+
lastQueueTime = System.nanoTime();
539+
}
540+
}
541+
scheduleEngineFrame();
542+
}
543+
544+
PerImage dequeueImage() {
545+
PerImage r = null;
546+
synchronized (lock) {
547+
for (PerImageReader reader : imageReaderQueue) {
548+
r = reader.dequeueImage();
549+
if (r == null) {
550+
// This reader is probably about to get pruned.
551+
continue;
552+
}
553+
if (VERBOSE_LOGS) {
554+
if (lastDequeueTime != 0) {
555+
long now = System.nanoTime();
556+
long dequeueDelta = now - lastDequeueTime;
557+
long queuedFor = now - r.queuedTime;
558+
long scheduleDelay = now - lastScheduleTime;
559+
Log.i(
560+
TAG,
561+
""
562+
+ reader.reader.hashCode()
563+
+ " dequeued image="
564+
+ r.image.hashCode()
565+
+ " queuedFor= "
566+
+ deltaMillis(queuedFor)
567+
+ " dequeueDelta="
568+
+ deltaMillis(dequeueDelta)
569+
+ " scheduleDelay="
570+
+ deltaMillis(scheduleDelay));
571+
lastDequeueTime = now;
572+
} else {
573+
lastDequeueTime = System.nanoTime();
574+
}
575+
}
576+
if (lastDequeuedImage != null) {
577+
if (VERBOSE_LOGS) {
578+
Log.i(
579+
TAG,
580+
""
581+
+ lastReaderDequeuedFrom.reader.hashCode()
582+
+ " closing image="
583+
+ lastDequeuedImage.image.hashCode());
584+
}
585+
// We must keep the last image dequeued open until we are done presenting
586+
// it. We have just dequeued a new image (r). Close the previously dequeued
587+
// image.
588+
lastDequeuedImage.image.close();
589+
lastDequeuedImage = null;
590+
}
591+
// Remember the last image and reader dequeued from. We do this because we must
592+
// keep both of these alive until we are done presenting the image.
593+
lastDequeuedImage = r;
594+
lastReaderDequeuedFrom = reader;
595+
break;
596+
}
597+
pruneImageReaderQueue();
598+
}
599+
return r;
600+
}
601+
602+
@Override
603+
public void onTrimMemory(int level) {
604+
if (!trimOnMemoryPressure) {
605+
return;
606+
}
607+
if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
608+
return;
609+
}
610+
synchronized (lock) {
611+
numTrims++;
612+
}
613+
cleanup();
614+
createNewReader = true;
615+
}
616+
617+
private void releaseInternal() {
618+
cleanup();
619+
released = true;
620+
}
621+
622+
private void cleanup() {
623+
synchronized (lock) {
624+
for (PerImageReader pir : perImageReaders.values()) {
625+
if (lastReaderDequeuedFrom == pir) {
626+
lastReaderDequeuedFrom = null;
627+
}
628+
pir.close();
629+
}
630+
perImageReaders.clear();
631+
if (lastDequeuedImage != null) {
632+
lastDequeuedImage.image.close();
633+
lastDequeuedImage = null;
634+
}
635+
if (lastReaderDequeuedFrom != null) {
636+
lastReaderDequeuedFrom.close();
637+
lastReaderDequeuedFrom = null;
638+
}
639+
imageReaderQueue.clear();
640+
}
641+
}
642+
643+
@TargetApi(API_LEVELS.API_33)
644+
private void waitOnFence(Image image) {
645+
try {
646+
SyncFence fence = image.getFence();
647+
fence.awaitForever();
648+
} catch (IOException e) {
649+
// Drop.
650+
}
651+
}
652+
653+
private void maybeWaitOnFence(Image image) {
654+
if (image == null) {
655+
return;
656+
}
657+
if (ignoringFence) {
658+
return;
659+
}
660+
if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
661+
// The fence API is only available on Android >= 33.
662+
waitOnFence(image);
663+
return;
664+
}
665+
// Log once per ImageTextureEntry.
666+
ignoringFence = true;
667+
Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33");
668+
}
419669

420670
ImageReaderSurfaceProducer(long id) {
421671
this.id = id;
@@ -662,8 +912,28 @@ public void disableFenceForTest() {
662912
}
663913

664914
@VisibleForTesting
665-
public int readersToCloseSize() {
666-
return readersToClose.size();
915+
public int numImageReaders() {
916+
synchronized (lock) {
917+
return imageReaderQueue.size();
918+
}
919+
}
920+
921+
@VisibleForTesting
922+
public int numTrims() {
923+
synchronized (lock) {
924+
return numTrims;
925+
}
926+
}
927+
928+
@VisibleForTesting
929+
public int numImages() {
930+
int r = 0;
931+
synchronized (lock) {
932+
for (PerImageReader reader : imageReaderQueue) {
933+
r += reader.imageQueue.size();
934+
}
935+
}
936+
return r;
667937
}
668938
}
669939

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
*

0 commit comments

Comments
 (0)