Browse Source

Merge pull request #2359 from Electrostat-Lab/fix-jmesurfaceview-mem-leak

[Android] Fix Android JmeSurfaceView Memory Leak
Ryan McDonough 6 months ago
parent
commit
b174045ee1

+ 11 - 2
jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -247,10 +247,19 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
             }
             }
 
 
             listener.destroy();
             listener.destroy();
-
+            // releases the view holder from the Android Input Resources
+            // releasing the view enables the context instance to be
+            // reclaimed by the GC.
+            // if not released; it leads to a weak reference leak
+            // disabling the destruction of the Context View Holder.
+            androidInput.setView(null);
+
+            // nullifying the references
+            // signals their memory to be reclaimed
             listener = null;
             listener = null;
             renderer = null;
             renderer = null;
             timer = null;
             timer = null;
+            androidInput = null;
 
 
             // do android specific cleaning here
             // do android specific cleaning here
             logger.fine("Display destroyed.");
             logger.fine("Display destroyed.");

+ 38 - 23
jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -239,11 +239,7 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
     }
     }
 
 
     private void removeGLSurfaceView() {
     private void removeGLSurfaceView() {
-        ((Activity) getContext()).runOnUiThread(() -> {
-            if (glSurfaceView != null) {
-                JmeSurfaceView.this.removeView(glSurfaceView);
-            }
-        });
+        ((Activity) getContext()).runOnUiThread(() -> JmeSurfaceView.this.removeView(glSurfaceView));
     }
     }
 
 
     @Override
     @Override
@@ -265,19 +261,34 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
     public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
     public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
         switch (event) {
         switch (event) {
             case ON_DESTROY:
             case ON_DESTROY:
-                /*destroy only if the policy flag is enabled*/
-                if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
-                    legacyApplication.stop(!isGLThreadPaused());
-                }
+                // activity is off the foreground stack
+                // activity is being destructed completely as a result of Activity#finish()
+                // this is a killable automata state!
+                jmeSurfaceViewLogger.log(Level.INFO, "Hosting Activity has been destructed.");
                 break;
                 break;
             case ON_PAUSE:
             case ON_PAUSE:
-                loseFocus();
+                // activity is still on the foreground stack but not
+                // on the topmost level or before transition to stopped/hidden or destroyed state
+                // as a result of dispatch to Activity#finish()
+                // activity is no longer visible and is out of foreground
+                if (((Activity) getContext()).isFinishing()) {
+                    if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
+                        legacyApplication.stop(!isGLThreadPaused());
+                    } else if (destructionPolicy == DestructionPolicy.KEEP_WHEN_FINISH) {
+                        jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running.");
+                    }
+                } else {
+                    loseFocus();
+                }
                 break;
                 break;
             case ON_RESUME:
             case ON_RESUME:
+                // activity is back to the topmost of the
+                // foreground stack
                 gainFocus();
                 gainFocus();
                 break;
                 break;
             case ON_STOP:
             case ON_STOP:
-                jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running");
+                // activity is out off the foreground stack or being destructed by a finishing dispatch
+                // this is a killable automata state!
                 break;
                 break;
         }
         }
     }
     }
@@ -404,13 +415,13 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
 
 
     @Override
     @Override
     public void destroy() {
     public void destroy() {
-        /*skip the destroy block if the invoking instance is null*/
-        if (legacyApplication == null) {
-            return;
+        if (glSurfaceView != null) {
+            removeGLSurfaceView();
+        }
+        if (legacyApplication != null) {
+            legacyApplication.destroy();
         }
         }
-        removeGLSurfaceView();
-        legacyApplication.destroy();
-        /*help the Dalvik Garbage collector to destruct the pointers, by making them nullptr*/
+        /*help the Dalvik Garbage collector to destruct the objects, by releasing their references*/
         /*context instances*/
         /*context instances*/
         legacyApplication = null;
         legacyApplication = null;
         appSettings = null;
         appSettings = null;
@@ -430,10 +441,10 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
         onRendererCompleted = null;
         onRendererCompleted = null;
         onExceptionThrown = null;
         onExceptionThrown = null;
         onLayoutDrawn = null;
         onLayoutDrawn = null;
-        /*nullifying the static memory (pushing zero to registers to prepare for a clean use)*/
         GameState.setLegacyApplication(null);
         GameState.setLegacyApplication(null);
         GameState.setFirstUpdatePassed(false);
         GameState.setFirstUpdatePassed(false);
-        jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed");
+        JmeAndroidSystem.setView(null);
+        jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed.");
     }
     }
 
 
     @Override
     @Override
@@ -516,11 +527,13 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
             /*register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
             /*register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
             if (getContext() instanceof LifecycleOwner) {
             if (getContext() instanceof LifecycleOwner) {
                 ((LifecycleOwner) getContext()).getLifecycle().addObserver(JmeSurfaceView.this);
                 ((LifecycleOwner) getContext()).getLifecycle().addObserver(JmeSurfaceView.this);
+                jmeSurfaceViewLogger.log(Level.INFO, "Command binding SurfaceView to the Activity Lifecycle.");
             }
             }
         } else {
         } else {
             /*un-register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
             /*un-register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
             if (getContext() instanceof LifecycleOwner) {
             if (getContext() instanceof LifecycleOwner) {
                 ((LifecycleOwner) getContext()).getLifecycle().removeObserver(JmeSurfaceView.this);
                 ((LifecycleOwner) getContext()).getLifecycle().removeObserver(JmeSurfaceView.this);
+                jmeSurfaceViewLogger.log(Level.INFO, "Command removing SurfaceView from the Activity Lifecycle.");
             }
             }
         }
         }
     }
     }
@@ -917,7 +930,7 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
     }
     }
 
 
     /**
     /**
-     * Determines whether the app context would be destructed
+     * Determines whether the app context would be destructed as a result of dispatching {@link Activity#finish()}
      * with the holder activity context in case of {@link DestructionPolicy#DESTROY_WHEN_FINISH} or be
      * with the holder activity context in case of {@link DestructionPolicy#DESTROY_WHEN_FINISH} or be
      * spared for a second use in case of {@link DestructionPolicy#KEEP_WHEN_FINISH}.
      * spared for a second use in case of {@link DestructionPolicy#KEEP_WHEN_FINISH}.
      * Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
      * Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
@@ -926,12 +939,14 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
      */
      */
     public enum DestructionPolicy {
     public enum DestructionPolicy {
         /**
         /**
-         * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication}).
+         * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication})
+         * as a result of dispatching {@link Activity#finish()}.
          */
          */
         DESTROY_WHEN_FINISH,
         DESTROY_WHEN_FINISH,
         /**
         /**
          * Spares the game context inside a static memory {@link GameState#legacyApplication}
          * Spares the game context inside a static memory {@link GameState#legacyApplication}
-         * when the activity context is destroyed, but the app stills in the background.
+         * when the activity context is destroyed dispatching {@link Activity#finish()}, but the {@link android.app.Application}
+         * stills in the background.
          */
          */
         KEEP_WHEN_FINISH
         KEEP_WHEN_FINISH
     }
     }