Skip to content

Commit

Permalink
[android] Fixes 'drawRenderNode called on a context with no surface' …
Browse files Browse the repository at this point in the history
…crash (flutter#33655)
  • Loading branch information
0xZOne authored Jun 13, 2022
1 parent 8833a18 commit 031fa76
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -844,24 +844,10 @@ void onTrimMemory(int level) {
flutterEngine.getDartExecutor().notifyLowMemoryWarning();
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
}
flutterEngine.getRenderer().onTrimMemory(level);
}
}

/**
* Invoke this from {@link android.app.Activity#onLowMemory()}.
*
* <p>A {@code Fragment} host must have its containing {@code Activity} forward this call so that
* the {@code Fragment} can then invoke this method.
*
* <p>This method sends a "memory pressure warning" message to Flutter over the "system channel".
*/
void onLowMemory() {
Log.v(TAG, "Forwarding onLowMemory() to FlutterEngine.");
ensureAlive();
flutterEngine.getDartExecutor().notifyLowMemoryWarning();
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
}

/**
* Ensures that this delegate has not been {@link #release()}'ed.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -980,19 +980,6 @@ public void onTrimMemory(int level) {
}
}

/**
* Callback invoked when memory is low.
*
* <p>This implementation forwards a memory pressure warning to the running Flutter app.
*/
@Override
public void onLowMemory() {
super.onLowMemory();
if (stillAttachedForEvent("onLowMemory")) {
delegate.onLowMemory();
}
}

/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by {@link
* FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when initializing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.view.TextureRegistry;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

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

@NonNull
private final Set<WeakReference<TextureRegistry.OnTrimMemoryListener>> onTrimMemoryListeners =
new HashSet<>();

@NonNull
private final FlutterUiDisplayListener flutterUiDisplayListener =
new FlutterUiDisplayListener() {
Expand Down Expand Up @@ -89,6 +98,39 @@ public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListene
flutterJNI.removeIsDisplayingFlutterUiListener(listener);
}

private void clearDeadListeners() {
final Iterator<WeakReference<OnTrimMemoryListener>> iterator = onTrimMemoryListeners.iterator();
while (iterator.hasNext()) {
WeakReference<OnTrimMemoryListener> listenerRef = iterator.next();
final OnTrimMemoryListener listener = listenerRef.get();
if (listener == null) {
iterator.remove();
}
}
}

/** Adds a listener that is invoked when a memory pressure warning was forward. */
@VisibleForTesting
/* package */ void addOnTrimMemoryListener(@NonNull OnTrimMemoryListener listener) {
// Purge dead listener to avoid accumulating.
clearDeadListeners();
onTrimMemoryListeners.add(new WeakReference<>(listener));
}

/**
* Removes a {@link OnTrimMemoryListener} that was added with {@link
* #addOnTrimMemoryListener(OnTrimMemoryListener)}.
*/
@VisibleForTesting
/* package */ void removeOnTrimMemoryListener(@NonNull OnTrimMemoryListener listener) {
for (WeakReference<OnTrimMemoryListener> listenerRef : onTrimMemoryListeners) {
if (listenerRef.get() == listener) {
onTrimMemoryListeners.remove(listenerRef);
break;
}
}
}

// ------ START TextureRegistry IMPLEMENTATION -----
/**
* Creates and returns a new {@link SurfaceTexture} managed by the Flutter engine that is also
Expand All @@ -112,20 +154,38 @@ public SurfaceTextureEntry registerSurfaceTexture(@NonNull SurfaceTexture surfac
new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
Log.v(TAG, "New SurfaceTexture ID: " + entry.id());
registerTexture(entry.id(), entry.textureWrapper());
addOnTrimMemoryListener(entry);
return entry;
}

final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry {
@Override
public void onTrimMemory(int level) {
final Iterator<WeakReference<OnTrimMemoryListener>> iterator = onTrimMemoryListeners.iterator();
while (iterator.hasNext()) {
WeakReference<OnTrimMemoryListener> listenerRef = iterator.next();
final OnTrimMemoryListener listener = listenerRef.get();
if (listener != null) {
listener.onTrimMemory(level);
} else {
// Purge cleared refs to avoid accumulating a lot of dead listener
iterator.remove();
}
}
}

final class SurfaceTextureRegistryEntry
implements TextureRegistry.SurfaceTextureEntry, TextureRegistry.OnTrimMemoryListener {
private final long id;
@NonNull private final SurfaceTextureWrapper textureWrapper;
private boolean released;
@Nullable private OnFrameConsumedListener listener;
@Nullable private OnTrimMemoryListener trimMemoryListener;
@Nullable private OnFrameConsumedListener frameConsumedListener;
private final Runnable onFrameConsumed =
new Runnable() {
@Override
public void run() {
if (listener != null) {
listener.onFrameConsumed();
if (frameConsumedListener != null) {
frameConsumedListener.onFrameConsumed();
}
}
};
Expand All @@ -149,6 +209,13 @@ public void run() {
}
}

@Override
public void onTrimMemory(int level) {
if (trimMemoryListener != null) {
trimMemoryListener.onTrimMemory(level);
}
}

private SurfaceTexture.OnFrameAvailableListener onFrameListener =
new SurfaceTexture.OnFrameAvailableListener() {
@Override
Expand All @@ -164,6 +231,10 @@ public void onFrameAvailable(@NonNull SurfaceTexture texture) {
}
};

private void removeListener() {
removeOnTrimMemoryListener(this);
}

@NonNull
public SurfaceTextureWrapper textureWrapper() {
return textureWrapper;
Expand All @@ -188,6 +259,7 @@ public void release() {
Log.v(TAG, "Releasing a SurfaceTexture (" + id + ").");
textureWrapper.release();
unregisterTexture(id);
removeListener();
released = true;
}

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

@Override
public void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {
this.listener = listener;
frameConsumedListener = listener;
}

@Override
public void setOnTrimMemoryListener(@Nullable OnTrimMemoryListener listener) {
trimMemoryListener = listener;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package io.flutter.plugin.platform;

import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
Expand Down Expand Up @@ -56,7 +58,7 @@ class PlatformViewWrapper extends FrameLayout {
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
private final AtomicLong pendingFramesCount = new AtomicLong(0L);

private final TextureRegistry.OnFrameConsumedListener listener =
private final TextureRegistry.OnFrameConsumedListener frameConsumedListener =
new TextureRegistry.OnFrameConsumedListener() {
@Override
public void onFrameConsumed() {
Expand All @@ -66,12 +68,40 @@ public void onFrameConsumed() {
}
};

private boolean shouldRecreateSurfaceForLowMemory = false;
private final TextureRegistry.OnTrimMemoryListener trimMemoryListener =
new TextureRegistry.OnTrimMemoryListener() {
@Override
public void onTrimMemory(int level) {
// When a memory pressure warning is received and the level equal {@code
// ComponentCallbacks2.TRIM_MEMORY_COMPLETE}, the Android system releases the underlying
// surface. If we continue to use the surface (e.g., call lockHardwareCanvas), a crash
// occurs, and we found that this crash appeared on Android10 and above.
// See https://github.com/flutter/flutter/issues/103870 for more details.
//
// Here our workaround is to recreate the surface before using it.
if (level == TRIM_MEMORY_COMPLETE && Build.VERSION.SDK_INT >= 29) {
shouldRecreateSurfaceForLowMemory = true;
}
}
};

private void onFrameProduced() {
if (Build.VERSION.SDK_INT == 29) {
pendingFramesCount.incrementAndGet();
}
}

private void recreateSurfaceIfNeeded() {
if (shouldRecreateSurfaceForLowMemory) {
if (surface != null) {
surface.release();
}
surface = createSurface(tx);
shouldRecreateSurfaceForLowMemory = false;
}
}

private boolean shouldDrawToSurfaceNow() {
if (Build.VERSION.SDK_INT == 29) {
return pendingFramesCount.get() <= 0L;
Expand All @@ -87,7 +117,8 @@ public PlatformViewWrapper(@NonNull Context context) {
public PlatformViewWrapper(
@NonNull Context context, @NonNull TextureRegistry.SurfaceTextureEntry textureEntry) {
this(context);
textureEntry.setOnFrameConsumedListener(listener);
textureEntry.setOnFrameConsumedListener(frameConsumedListener);
textureEntry.setOnTrimMemoryListener(trimMemoryListener);
setTexture(textureEntry.surfaceTexture());
}

Expand Down Expand Up @@ -249,6 +280,10 @@ public void draw(Canvas canvas) {
// If there are still frames that are not consumed, we will draw them next time.
invalidate();
} else {
// We try to recreate the surface before using it to avoid the crash:
// https://github.com/flutter/flutter/issues/103870
recreateSurfaceIfNeeded();

// Override the canvas that this subtree of views will use to draw.
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
try {
Expand Down
16 changes: 16 additions & 0 deletions shell/platform/android/io/flutter/view/TextureRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public interface TextureRegistry {
@NonNull
SurfaceTextureEntry registerSurfaceTexture(@NonNull SurfaceTexture surfaceTexture);

/**
* Callback invoked when memory is low.
*
* <p>Invoke this from {@link android.app.Activity#onTrimMemory(int)}.
*/
default void onTrimMemory(int level) {}

/** A registry entry for a managed SurfaceTexture. */
interface SurfaceTextureEntry {
/** @return The managed SurfaceTexture. */
Expand All @@ -45,6 +52,9 @@ interface SurfaceTextureEntry {

/** Set a listener that will be notified when the most recent image has been consumed. */
default void setOnFrameConsumedListener(@Nullable OnFrameConsumedListener listener) {}

/** Set a listener that will be notified when a memory pressure warning was forward. */
default void setOnTrimMemoryListener(@Nullable OnTrimMemoryListener listener) {}
}

/** Listener invoked when the most recent image has been consumed. */
Expand All @@ -55,4 +65,10 @@ interface OnFrameConsumedListener {
*/
void onFrameConsumed();
}

/** Listener invoked when a memory pressure warning was forward. */
interface OnTrimMemoryListener {
/** This method will be invoked when a memory pressure warning was forward. */
void onTrimMemory(int level);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -863,23 +863,6 @@ public void itNotifiesDartExecutorAndSendsMessageOverSystemChannelWhenToldToTrim
verify(mockFlutterEngine.getSystemChannel(), times(6)).sendMemoryPressureWarning();
}

@Test
public void itNotifiesDartExecutorAndSendsMessageOverSystemChannelWhenInformedOfLowMemory() {
// Create the real object that we're testing.
FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);

// --- Execute the behavior under test ---
// The FlutterEngine is set up in onAttach().
delegate.onAttach(ctx);

// Emulate the host and call the method that we expect to be forwarded.
delegate.onLowMemory();

// Verify that the call was forwarded to the engine.
verify(mockFlutterEngine.getDartExecutor(), times(1)).notifyLowMemoryWarning();
verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning();
}

@Test
public void itDestroysItsOwnEngineIfHostRequestsIt() {
// ---- Test setup ----
Expand Down
Loading

0 comments on commit 031fa76

Please sign in to comment.