Skip to content

[Android] Fix Android JmeSurfaceView Memory Leak #2359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2009-2023 jMonkeyEngine
* Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -247,10 +247,19 @@ protected void deinitInThread() {
}

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;
renderer = null;
timer = null;
androidInput = null;

// do android specific cleaning here
logger.fine("Display destroyed.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2009-2022 jMonkeyEngine
* Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -239,11 +239,7 @@ public void startRenderer(int delayMillis) {
}

private void removeGLSurfaceView() {
((Activity) getContext()).runOnUiThread(() -> {
if (glSurfaceView != null) {
JmeSurfaceView.this.removeView(glSurfaceView);
}
});
((Activity) getContext()).runOnUiThread(() -> JmeSurfaceView.this.removeView(glSurfaceView));
}

@Override
Expand All @@ -265,19 +261,34 @@ public void handleError(String errorMsg, Throwable throwable) {
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
switch (event) {
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;
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;
case ON_RESUME:
// activity is back to the topmost of the
// foreground stack
gainFocus();
break;
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;
}
}
Expand Down Expand Up @@ -404,13 +415,13 @@ public void loseFocus() {

@Override
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*/
legacyApplication = null;
appSettings = null;
Expand All @@ -430,10 +441,10 @@ public void destroy() {
onRendererCompleted = null;
onExceptionThrown = null;
onLayoutDrawn = null;
/*nullifying the static memory (pushing zero to registers to prepare for a clean use)*/
GameState.setLegacyApplication(null);
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
Expand Down Expand Up @@ -516,11 +527,13 @@ public void bindAppStateToActivityLifeCycle(final boolean condition) {
/*register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
if (getContext() instanceof LifecycleOwner) {
((LifecycleOwner) getContext()).getLifecycle().addObserver(JmeSurfaceView.this);
jmeSurfaceViewLogger.log(Level.INFO, "Command binding SurfaceView to the Activity Lifecycle.");
}
} else {
/*un-register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
if (getContext() instanceof LifecycleOwner) {
((LifecycleOwner) getContext()).getLifecycle().removeObserver(JmeSurfaceView.this);
jmeSurfaceViewLogger.log(Level.INFO, "Command removing SurfaceView from the Activity Lifecycle.");
}
}
}
Expand Down Expand Up @@ -917,7 +930,7 @@ public void setShowErrorDialog(boolean showErrorDialog) {
}

/**
* 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
* spared for a second use in case of {@link DestructionPolicy#KEEP_WHEN_FINISH}.
* Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
Expand All @@ -926,12 +939,14 @@ public void setShowErrorDialog(boolean showErrorDialog) {
*/
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,
/**
* 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
}
Expand Down