Navigation组件中Fragment因SpecialEffectsController操作泄漏求助
Fragment切换内存泄漏问题分析与解决
问题背景
使用以下依赖版本:
- Navigation Component 2.9.0
- androidx Fragments 1.8.6
- androidx.transitions 1.6.0
已启用Predictive Back和手势导航,在导航图中频繁切换Fragment时,出现LoginFragment内存泄漏,LeakCanary检测日志如下:
┬─── │ GC Root: Thread object │ ├─ java.lang.Thread instance │ Leaking: NO (the main thread always runs) │ Thread name: 'main' │ ↓ Thread.threadLocals │ ~~~~~~~~~~~~ ├─ java.lang.ThreadLocal$ThreadLocalMap instance │ Leaking: UNKNOWN │ Retaining 1.5 MB in 17658 objects │ ↓ ThreadLocal$ThreadLocalMap.table │ ~~~~~ ├─ java.lang.ThreadLocal$ThreadLocalMap$Entry[] array │ Leaking: UNKNOWN │ Retaining 1.5 MB in 17657 objects │ ↓ ThreadLocal$ThreadLocalMap$Entry[5] │ ~~~ ├─ java.lang.ThreadLocal$ThreadLocalMap$Entry instance │ Leaking: UNKNOWN │ Retaining 1.5 MB in 17534 objects │ ↓ ThreadLocal$ThreadLocalMap$Entry.value │ ~~~~~ ├─ androidx.collection.ArrayMap instance │ Leaking: UNKNOWN │ Retaining 1.5 MB in 17533 objects │ ↓ SimpleArrayMap.array │ ~~~~~ ├─ java.lang.Object[] array │ Leaking: UNKNOWN │ Retaining 1.5 MB in 17531 objects │ ↓ Object[1] │ ~~~ ├─ androidx.transition.Transition$AnimationInfo instance │ Leaking: UNKNOWN │ Retaining 32 B in 1 objects │ ↓ Transition$AnimationInfo.mTransition │ ~~~~~~~~~~~ ├─ androidx.transition.Slide instance │ Leaking: UNKNOWN │ Retaining 371.6 kB in 9596 objects │ ↓ Transition.mCloneParent │ ~~~~~~~~~~~~ ├─ androidx.transition.Slide instance │ Leaking: UNKNOWN │ Retaining 800 B in 17 objects │ ↓ Transition.mParent │ ~~~~~~~ ├─ androidx.transition.TransitionSet instance │ Leaking: UNKNOWN │ Retaining 861 B in 18 objects │ ↓ Transition.mParent │ ~~~~~~~ ├─ androidx.transition.TransitionSet instance │ Leaking: UNKNOWN │ Retaining 1.9 kB in 60 objects │ ↓ Transition.mListeners │ ~~~~~~~~~~ ├─ java.util.ArrayList instance │ Leaking: UNKNOWN │ Retaining 1.0 kB in 41 objects │ ↓ ArrayList[1] │ ~~~ ├─ androidx.transition.FragmentTransitionSupport$4 instance │ Leaking: UNKNOWN │ Retaining 16 B in 1 objects │ Anonymous class implementing androidx.transition. │ Transition$TransitionListener │ ↓ FragmentTransitionSupport$4.val$transitionCompleteRunnable │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ├─ androidx.fragment.app. │ DefaultSpecialEffectsController$TransitionEffect$$ExternalSyntheticLambda4 │ instance │ Leaking: UNKNOWN │ Retaining 16 B in 1 objects │ ↓ │ DefaultSpecialEffectsController$TransitionEffect$$ExternalSyntheticLambda4. │ f$0 │ ~~~ ├─ androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation │ instance │ Leaking: UNKNOWN │ Retaining 237 B in 9 objects │ ↓ SpecialEffectsController$Operation.fragment │ ~~~~~~~~ ╰→ com.app.login.LoginFragment instance Leaking: YES (ObjectWatcher was watching this because com.app. login.LoginFragment received Fragment#onDestroy() callback. Conflicts with Fragment.mLifecycleRegistry.state is INITIALIZED) Retaining 8.2 kB in 200 objects key = 273e40d7-b2ad-4f91-b6a1-9af85e1c1ffd watchDurationMillis = 6300 retainedDurationMillis = 1300 componentContext instance of dagger.hilt.android.internal.managers. ViewComponentManager$FragmentContextWrapper, wrapping activity com. app.MainActivity with mDestroyed = false METADATA Build.VERSION.SDK_INT: 34 Build.MANUFACTURER: samsung LeakCanary version: 2.14 App process name: com.app Class count: 37912 Instance count: 282042 Primitive array count: 187899 Object array count: 46537 Thread count: 71 Heap total bytes: 38826738 Bitmap count: 6 Bitmap total bytes: 1283848 Large bitmap count: 0 Large bitmap total bytes: 0 Db 1: closed /data/user/0/com.app. databases/google_app_measurement_local.db Db 2: open /data/user/0/com.app/databases/com.google.android. datatransport.events Db 3: open /data/user/0/com.app/databases/app_db Stats: LruCache[maxSize=3000,hits=130652,misses=244920,hitRate=34%] RandomAccess[bytes=12181917,reads=244920,travel=126574568852,range=46016226,size =57532227] Analysis duration: 19115 ms
泄漏原因分析
泄漏链显示:主线程ThreadLocal持有Transition相关对象,Transition的监听列表中保留了匿名TransitionListener,该Listener持有关联LoginFragment的Runnable,导致Fragment在onDestroy()后无法被GC回收。这是特定版本下Fragment、Transition组件与Predictive Back组合的已知兼容性问题。
解决方案
1. 升级依赖版本(最优方案)
Google已在后续版本修复该类泄漏,建议升级到以下稳定版本:
- androidx Fragments:1.8.7及以上
- androidx.transitions:1.6.1及以上
- Navigation Component:2.9.1及以上(保持与Fragment版本兼容)
2. 禁用过渡动画(临时方案)
若无法立即升级,可对泄漏频发的导航动作禁用动画,避免Transition持有Fragment引用:
findNavController().navigate( R.id.action_login_to_target, null, NavOptions.Builder() .setEnterAnim(0) .setExitAnim(0) .setPopEnterAnim(0) .setPopExitAnim(0) .build() )
3. 手动清理Transition监听(进阶方案)
若需保留动画,可在FragmentonDestroy()中通过反射清理Transition监听(仅作参考,需适配实际场景):
override fun onDestroy() { super.onDestroy() try { val controllerField = javaClass.getDeclaredField("mSpecialEffectsController") controllerField.isAccessible = true val controller = controllerField.get(this) as? SpecialEffectsController controller?.let { val transitionsField = it::class.java.getDeclaredField("mTransitions") transitionsField.isAccessible = true val transitions = transitionsField.get(it) as? List<Transition> transitions?.forEach { transition -> transition.removeAllListeners() } } } catch (e: Exception) { e.printStackTrace() } }
4. 检查Predictive Back适配
确保自定义OnBackPressedCallback使用WeakReference持有Fragment实例,避免强引用导致额外泄漏。
内容的提问来源于stack exchange,提问作者B W
相关产品推荐
相关产品推荐

