SDK / artifact
com.synerise.sdk:synerise-mobile-sdk
Confirmed in 6.13.0 and 6.14.0 (sources jar).
Summary
InAppRenderingManager is a process-wide singleton (held via the static instance field). Its private Dialog dialog field is assigned exactly once, when the dialog is created, and is NEVER set back to null. Every dismissal path only calls dialog.cancel()/dismiss(); none clears the field. As a result the singleton keeps holding the dismissed Dialog, and through Dialog.mContext it keeps holding the Activity the dialog was created with — until the next in-app overwrites the field. When that Activity is destroyed before the next in-app, it leaks.
Source evidence (InAppRenderingManager.java)
private Dialog dialog; // ~line 87
- The ONLY assignment (inside
show(...)):
dialog = new Dialog(currentActivity, ...); // ~line 128
- Dismissal paths cancel but never null the field:
OnDismissListener.onDismiss(...) (~line 176-184)
dismissInAppDialog() (~line 753-777)
performInAppClose(...) -> dismissInAppDialog() (~line 742-751)
In all of them: dialog.cancel() is called, but dialog = null is never set.
(grep for "dialog =" returns only the declaration-time new Dialog(...) and a dialog == null null-check; there is no clearing assignment anywhere.)
Reproduction
- Trigger an in-app message while some Activity is in the foreground.
- Dismiss the in-app (tap close, JS close/hide, or it is dismissed when leaving) and finish/destroy that Activity.
- Observe with LeakCanary.
LeakCanary trace (abridged)
GC Root: Global variable in native code
...
static InAppRenderingManager.instance
-> InAppRenderingManager.dialog
-> Dialog.mContext (android.view.ContextThemeWrapper)
wrapping activity <HostActivity> with mDestroyed = true
Dialog#mDecor is null // i.e. the dialog was dismissed
-> ContextWrapper.mBase
=> <HostActivity> instance (LEAKING)
Dialog#mDecor is null confirms the dialog was already dismissed; the only thing still retaining the destroyed Activity is the un-cleared InAppRenderingManager.dialog field.
Impact
- Every dismissed in-app leaves a dangling reference to its host Activity.
- The reference becomes a real Activity leak whenever that Activity is destroyed before the next in-app is shown (e.g. user closes the screen after seeing an in-app).
- Retains the whole Activity view tree + WebView.
Requested fix
Null the dialog field after it is dismissed. The natural place is the OnDismissListener.onDismiss() callback (and/or dismissInAppDialog()), e.g.:
dialog.setOnDismissListener(d -> {
isCampaignCurrentlyShown = false;
currentInAppCampaignHash = null;
inAppListener.onDismissed(inAppMessageData);
dialog = null; // <-- release the Dialog (and its Activity context)
});
Please also review related per-dialog fields captured by the singleton/handlers (e.g. viewCreatingHandler, the WebView) so the whole view tree is released on dismissal.
SDK / artifact
com.synerise.sdk:synerise-mobile-sdkConfirmed in 6.13.0 and 6.14.0 (sources jar).
Summary
InAppRenderingManageris a process-wide singleton (held via the staticinstancefield). Itsprivate Dialog dialogfield is assigned exactly once, when the dialog is created, and is NEVER set back to null. Every dismissal path only callsdialog.cancel()/dismiss();none clears the field. As a result the singleton keeps holding the dismissed Dialog, and throughDialog.mContextit keeps holding the Activity the dialog was created with — until the next in-app overwrites the field. When that Activity is destroyed before the next in-app, it leaks.Source evidence (
InAppRenderingManager.java)show(...)):OnDismissListener.onDismiss(...)(~line 176-184)dismissInAppDialog()(~line 753-777)performInAppClose(...) -> dismissInAppDialog()(~line 742-751)In all of them:
dialog.cancel()is called, butdialog = nullis never set.(
grep for "dialog =" returns only the declaration-time new Dialog(...) and a dialog == null null-check; there is no clearing assignment anywhere.)Reproduction
LeakCanary trace (abridged)
Dialog#mDecor is nullconfirms the dialog was already dismissed; the only thing still retaining the destroyed Activity is the un-clearedInAppRenderingManager.dialogfield.Impact
Requested fix
Null the
dialogfield after it is dismissed. The natural place is theOnDismissListener.onDismiss()callback (and/ordismissInAppDialog()), e.g.:Please also review related per-dialog fields captured by the singleton/handlers (e.g.
viewCreatingHandler, theWebView) so the whole view tree is released on dismissal.