3

I'm having a problem with memory leak in EnterTransitionCoordinator while using shared element transitions. Below you can see the app structure:

App structure

It has 2 screens, first is an Activity with DrawerLayout and few Fragments inside. One of them consists a list of photos and clicking specific photo triggers shared element transition to Fragment from ViewPager located in another Activity. I'm using custom SharedElementCallback when exiting and reentering these two Activitys for mapping a correct View for shared element transition. I based my code on this great blog post: https://android.jlelse.eu/dynamic-shared-element-transition-23428f62a2af

The problem is, that after swiping between ViewPager's items, Fragments are being destroyed, but the View used for shared element transition is being kept in Activity's ActivityTransitionState, specifically in EnterTransitionCoordinator. The same when reentering to Activity with DrawerLayout and then opening another Fragment. References to Views used for shared element transitions are still kept int Activitys even though Fragments were destroyed, which causes a memory leak.

My question: Is there a good way to avoid this memory leak?

1 Answers1

2

I discovered that there's a method clearState() in EnterTransitionCoordinator, which should be called in Activity.onStop(). But since the Activity is not yet being stopped, Views from Fragments are being leaked. As a temporary workaround, I'm clearing that state manually on Fragment.onDestroyView() by calling this method with reflection. Below you can see the code:

/**
 * Works only for API < 28
 * https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces
 */
fun Fragment.clearEnterTransitionState() {
    try {
        getActivityTransitionState()
            ?.getEnterTransitionCoordinator()
            ?.invokeClearStateMethod()
    } catch (e: Exception) {
        // no-op
    }
}

private fun Fragment.getActivityTransitionState() =
    Activity::class.java.getField("mActivityTransitionState", requireActivity())

private fun Any.getEnterTransitionCoordinator() = javaClass.getField("mEnterTransitionCoordinator", this)

private fun Any.invokeClearStateMethod() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
        javaClass.superclass?.invokeClearStateMethod(this)
    } else {
        javaClass.invokeClearStateMethod(this)
    }
}

private fun <T> Class<T>.getField(name: String, target: Any): Any? =
    getDeclaredField(name).run {
        isAccessible = true
        get(target)
    }

private fun <T> Class<T>.invokeClearStateMethod(target: Any) {
    getDeclaredMethod("clearState").apply {
        isAccessible = true
        invoke(target)
    }
}
  • Its probably better not to do this. If you're actually low on memory, the framework will destroy the previous activity, which will clear this up for you. The only case where it may be a problem is if you have a single, very large allocation to make and need to avoid running OOM on it. This would also break any animation on the back button. – Gabe Sechan Dec 22 '18 at 22:02
  • @GabeSechan sure, I agree it's not the best way to fix it. Actually, it's not breaking any animation on back button since custom SharedElementCallback is set to map correct view for transition. – Grzegorz Matyszczak Dec 23 '18 at 22:41