Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 031fa76

Browse files
authored
[android] Fixes 'drawRenderNode called on a context with no surface' crash (#33655)
1 parent 8833a18 commit 031fa76

File tree

7 files changed

+190
-52
lines changed

7 files changed

+190
-52
lines changed

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

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -844,24 +844,10 @@ void onTrimMemory(int level) {
844844
flutterEngine.getDartExecutor().notifyLowMemoryWarning();
845845
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
846846
}
847+
flutterEngine.getRenderer().onTrimMemory(level);
847848
}
848849
}
849850

850-
/**
851-
* Invoke this from {@link android.app.Activity#onLowMemory()}.
852-
*
853-
* <p>A {@code Fragment} host must have its containing {@code Activity} forward this call so that
854-
* the {@code Fragment} can then invoke this method.
855-
*
856-
* <p>This method sends a "memory pressure warning" message to Flutter over the "system channel".
857-
*/
858-
void onLowMemory() {
859-
Log.v(TAG, "Forwarding onLowMemory() to FlutterEngine.");
860-
ensureAlive();
861-
flutterEngine.getDartExecutor().notifyLowMemoryWarning();
862-
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
863-
}
864-
865851
/**
866852
* Ensures that this delegate has not been {@link #release()}'ed.
867853
*

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

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -980,19 +980,6 @@ public void onTrimMemory(int level) {
980980
}
981981
}
982982

983-
/**
984-
* Callback invoked when memory is low.
985-
*
986-
* <p>This implementation forwards a memory pressure warning to the running Flutter app.
987-
*/
988-
@Override
989-
public void onLowMemory() {
990-
super.onLowMemory();
991-
if (stillAttachedForEvent("onLowMemory")) {
992-
delegate.onLowMemory();
993-
}
994-
}
995-
996983
/**
997984
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
998985
* FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when initializing

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

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@
1212
import android.view.Surface;
1313
import androidx.annotation.NonNull;
1414
import androidx.annotation.Nullable;
15+
import androidx.annotation.VisibleForTesting;
1516
import io.flutter.Log;
1617
import io.flutter.embedding.engine.FlutterJNI;
1718
import io.flutter.view.TextureRegistry;
19+
import java.lang.ref.WeakReference;
1820
import java.nio.ByteBuffer;
1921
import java.util.ArrayList;
22+
import java.util.HashSet;
23+
import java.util.Iterator;
2024
import java.util.List;
25+
import java.util.Set;
2126
import java.util.concurrent.atomic.AtomicLong;
2227

2328
/**
@@ -42,6 +47,10 @@ public class FlutterRenderer implements TextureRegistry {
4247
private boolean isDisplayingFlutterUi = false;
4348
private Handler handler = new Handler();
4449

50+
@NonNull
51+
private final Set<WeakReference<TextureRegistry.OnTrimMemoryListener>> onTrimMemoryListeners =
52+
new HashSet<>();
53+
4554
@NonNull
4655
private final FlutterUiDisplayListener flutterUiDisplayListener =
4756
new FlutterUiDisplayListener() {
@@ -89,6 +98,39 @@ public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListene
8998
flutterJNI.removeIsDisplayingFlutterUiListener(listener);
9099
}
91100

101+
private void clearDeadListeners() {
102+
final Iterator<WeakReference<OnTrimMemoryListener>> iterator = onTrimMemoryListeners.iterator();
103+
while (iterator.hasNext()) {
104+
WeakReference<OnTrimMemoryListener> listenerRef = iterator.next();
105+
final OnTrimMemoryListener listener = listenerRef.get();
106+
if (listener == null) {
107+
iterator.remove();
108+
}
109+
}
110+
}
111+
112+
/** Adds a listener that is invoked when a memory pressure warning was forward. */
113+
@VisibleForTesting
114+
/* package */ void addOnTrimMemoryListener(@NonNull OnTrimMemoryListener listener) {
115+
// Purge dead listener to avoid accumulating.
116+
clearDeadListeners();
117+
onTrimMemoryListeners.add(new WeakReference<>(listener));
118+
}
119+
120+
/**
121+
* Removes a {@link OnTrimMemoryListener} that was added with {@link
122+
* #addOnTrimMemoryListener(OnTrimMemoryListener)}.
123+
*/
124+
@VisibleForTesting
125+
/* package */ void removeOnTrimMemoryListener(@NonNull OnTrimMemoryListener listener) {
126+
for (WeakReference<OnTrimMemoryListener> listenerRef : onTrimMemoryListeners) {
127+
if (listenerRef.get() == listener) {
128+
onTrimMemoryListeners.remove(listenerRef);
129+
break;
130+
}
131+
}
132+
}
133+
92134
// ------ START TextureRegistry IMPLEMENTATION -----
93135
/**
94136
* Creates and returns a new {@link SurfaceTexture} managed by the Flutter engine that is also
@@ -112,20 +154,38 @@ public SurfaceTextureEntry registerSurfaceTexture(@NonNull SurfaceTexture surfac
112154
new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
113155
Log.v(TAG, "New SurfaceTexture ID: " + entry.id());
114156
registerTexture(entry.id(), entry.textureWrapper());
157+
addOnTrimMemoryListener(entry);
115158
return entry;
116159
}
117160

118-
final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry {
161+
@Override
162+
public void onTrimMemory(int level) {
163+
final Iterator<WeakReference<OnTrimMemoryListener>> iterator = onTrimMemoryListeners.iterator();
164+
while (iterator.hasNext()) {
165+
WeakReference<OnTrimMemoryListener> listenerRef = iterator.next();
166+
final OnTrimMemoryListener listener = listenerRef.get();
167+
if (listener != null) {
168+
listener.onTrimMemory(level);
169+
} else {
170+
// Purge cleared refs to avoid accumulating a lot of dead listener
171+
iterator.remove();
172+
}
173+
}
174+
}
175+
176+
final class SurfaceTextureRegistryEntry
177+
implements TextureRegistry.SurfaceTextureEntry, TextureRegistry.OnTrimMemoryListener {
119178
private final long id;
120179
@NonNull private final SurfaceTextureWrapper textureWrapper;
121180
private boolean released;
122-
@Nullable private OnFrameConsumedListener listener;
181+
@Nullable private OnTrimMemoryListener trimMemoryListener;
182+
@Nullable private OnFrameConsumedListener frameConsumedListener;
123183
private final Runnable onFrameConsumed =
124184
new Runnable() {
125185
@Override
126186
public void run() {
127-
if (listener != null) {
128-
listener.onFrameConsumed();
187+
if (frameConsumedListener != null) {
188+
frameConsumedListener.onFrameConsumed();
129189
}
130190
}
131191
};
@@ -149,6 +209,13 @@ public void run() {
149209
}
150210
}
151211

212+
@Override
213+
public void onTrimMemory(int level) {
214+
if (trimMemoryListener != null) {
215+
trimMemoryListener.onTrimMemory(level);
216+
}
217+
}
218+
152219
private SurfaceTexture.OnFrameAvailableListener onFrameListener =
153220
new SurfaceTexture.OnFrameAvailableListener() {
154221
@Override
@@ -164,6 +231,10 @@ public void onFrameAvailable(@NonNull SurfaceTexture texture) {
164231
}
165232
};
166233

234+
private void removeListener() {
235+
removeOnTrimMemoryListener(this);
236+
}
237+
167238
@NonNull
168239
public SurfaceTextureWrapper textureWrapper() {
169240
return textureWrapper;
@@ -188,6 +259,7 @@ public void release() {
188259
Log.v(TAG, "Releasing a SurfaceTexture (" + id + ").");
189260
textureWrapper.release();
190261
unregisterTexture(id);
262+
removeListener();
191263
released = true;
192264
}
193265

@@ -206,7 +278,12 @@ protected void finalize() throws Throwable {
206278

207279
@Override
208280
public void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {
209-
this.listener = listener;
281+
frameConsumedListener = listener;
282+
}
283+
284+
@Override
285+
public void setOnTrimMemoryListener(@Nullable OnTrimMemoryListener listener) {
286+
trimMemoryListener = listener;
210287
}
211288
}
212289

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package io.flutter.plugin.platform;
66

7+
import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
8+
79
import android.annotation.SuppressLint;
810
import android.annotation.TargetApi;
911
import android.content.Context;
@@ -56,7 +58,7 @@ class PlatformViewWrapper extends FrameLayout {
5658
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
5759
private final AtomicLong pendingFramesCount = new AtomicLong(0L);
5860

59-
private final TextureRegistry.OnFrameConsumedListener listener =
61+
private final TextureRegistry.OnFrameConsumedListener frameConsumedListener =
6062
new TextureRegistry.OnFrameConsumedListener() {
6163
@Override
6264
public void onFrameConsumed() {
@@ -66,12 +68,40 @@ public void onFrameConsumed() {
6668
}
6769
};
6870

71+
private boolean shouldRecreateSurfaceForLowMemory = false;
72+
private final TextureRegistry.OnTrimMemoryListener trimMemoryListener =
73+
new TextureRegistry.OnTrimMemoryListener() {
74+
@Override
75+
public void onTrimMemory(int level) {
76+
// When a memory pressure warning is received and the level equal {@code
77+
// ComponentCallbacks2.TRIM_MEMORY_COMPLETE}, the Android system releases the underlying
78+
// surface. If we continue to use the surface (e.g., call lockHardwareCanvas), a crash
79+
// occurs, and we found that this crash appeared on Android10 and above.
80+
// See https://github.com/flutter/flutter/issues/103870 for more details.
81+
//
82+
// Here our workaround is to recreate the surface before using it.
83+
if (level == TRIM_MEMORY_COMPLETE && Build.VERSION.SDK_INT >= 29) {
84+
shouldRecreateSurfaceForLowMemory = true;
85+
}
86+
}
87+
};
88+
6989
private void onFrameProduced() {
7090
if (Build.VERSION.SDK_INT == 29) {
7191
pendingFramesCount.incrementAndGet();
7292
}
7393
}
7494

95+
private void recreateSurfaceIfNeeded() {
96+
if (shouldRecreateSurfaceForLowMemory) {
97+
if (surface != null) {
98+
surface.release();
99+
}
100+
surface = createSurface(tx);
101+
shouldRecreateSurfaceForLowMemory = false;
102+
}
103+
}
104+
75105
private boolean shouldDrawToSurfaceNow() {
76106
if (Build.VERSION.SDK_INT == 29) {
77107
return pendingFramesCount.get() <= 0L;
@@ -87,7 +117,8 @@ public PlatformViewWrapper(@NonNull Context context) {
87117
public PlatformViewWrapper(
88118
@NonNull Context context, @NonNull TextureRegistry.SurfaceTextureEntry textureEntry) {
89119
this(context);
90-
textureEntry.setOnFrameConsumedListener(listener);
120+
textureEntry.setOnFrameConsumedListener(frameConsumedListener);
121+
textureEntry.setOnTrimMemoryListener(trimMemoryListener);
91122
setTexture(textureEntry.surfaceTexture());
92123
}
93124

@@ -249,6 +280,10 @@ public void draw(Canvas canvas) {
249280
// If there are still frames that are not consumed, we will draw them next time.
250281
invalidate();
251282
} else {
283+
// We try to recreate the surface before using it to avoid the crash:
284+
// https://github.com/flutter/flutter/issues/103870
285+
recreateSurfaceIfNeeded();
286+
252287
// Override the canvas that this subtree of views will use to draw.
253288
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
254289
try {

shell/platform/android/io/flutter/view/TextureRegistry.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ public interface TextureRegistry {
3131
@NonNull
3232
SurfaceTextureEntry registerSurfaceTexture(@NonNull SurfaceTexture surfaceTexture);
3333

34+
/**
35+
* Callback invoked when memory is low.
36+
*
37+
* <p>Invoke this from {@link android.app.Activity#onTrimMemory(int)}.
38+
*/
39+
default void onTrimMemory(int level) {}
40+
3441
/** A registry entry for a managed SurfaceTexture. */
3542
interface SurfaceTextureEntry {
3643
/** @return The managed SurfaceTexture. */
@@ -45,6 +52,9 @@ interface SurfaceTextureEntry {
4552

4653
/** Set a listener that will be notified when the most recent image has been consumed. */
4754
default void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {}
55+
56+
/** Set a listener that will be notified when a memory pressure warning was forward. */
57+
default void setOnTrimMemoryListener(@Nullable OnTrimMemoryListener listener) {}
4858
}
4959

5060
/** Listener invoked when the most recent image has been consumed. */
@@ -55,4 +65,10 @@ interface OnFrameConsumedListener {
5565
*/
5666
void onFrameConsumed();
5767
}
68+
69+
/** Listener invoked when a memory pressure warning was forward. */
70+
interface OnTrimMemoryListener {
71+
/** This method will be invoked when a memory pressure warning was forward. */
72+
void onTrimMemory(int level);
73+
}
5874
}

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -863,23 +863,6 @@ public void itNotifiesDartExecutorAndSendsMessageOverSystemChannelWhenToldToTrim
863863
verify(mockFlutterEngine.getSystemChannel(), times(6)).sendMemoryPressureWarning();
864864
}
865865

866-
@Test
867-
public void itNotifiesDartExecutorAndSendsMessageOverSystemChannelWhenInformedOfLowMemory() {
868-
// Create the real object that we're testing.
869-
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
870-
871-
// --- Execute the behavior under test ---
872-
// The FlutterEngine is set up in onAttach().
873-
delegate.onAttach(ctx);
874-
875-
// Emulate the host and call the method that we expect to be forwarded.
876-
delegate.onLowMemory();
877-
878-
// Verify that the call was forwarded to the engine.
879-
verify(mockFlutterEngine.getDartExecutor(), times(1)).notifyLowMemoryWarning();
880-
verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning();
881-
}
882-
883866
@Test
884867
public void itDestroysItsOwnEngineIfHostRequestsIt() {
885868
// ---- Test setup ----

0 commit comments

Comments
 (0)