Fragment中SavedStateHandlesVM实例内存泄漏排查求助
GamesFragment导航时内存泄漏排查与解决方法
我有一个从API获取数据的GamesFragment,当导航至其他Fragment时出现内存泄漏。使用LeakCanary库监测到了泄漏日志,但无法定位原因。
LeakCanary泄漏日志
210 bytes retained by leaking objects D Signature: 135947a80c7aa2aa27381bdc2deb41d0342a02cb D ┬─── D │ GC Root: Global variable in native code D │ D ├─ java.util.HashMap instance D │ Leaking: NO (o↓ is not leaking) D │ ↓ HashMap["view"] D ├─ com.google.android.gms.ads.nonagon.ad.webview.o instance D │ Leaking: NO (MainActivity↓ is not leaking and View attached) D │ View is part of a window view hierarchy D │ View.mAttachInfo is not null (view attached) D │ View.mWindowAttachCount = 1 D │ mContext instance of xxx.xxx.xxxx.activity.MainActivity with mDestroyed = false D │ ↓ View.mContext D ├─ xxx.xxx.xxxx.activity.MainActivity instance D │ Leaking: NO (GamesFragment↓ is not leaking and Activity#mDestroyed is false) D │ mApplication instance of xxx.xxx.xxxx.BaseApplication D │ mBase instance of androidx.appcompat.view.ContextThemeWrapper D │ ↓ ComponentActivity.mOnConfigurationChangedListeners D ├─ java.util.concurrent.CopyOnWriteArrayList instance D │ Leaking: NO (GamesFragment↓ is not leaking) D │ ↓ CopyOnWriteArrayList[4] D ├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance D │ Leaking: NO (GamesFragment↓ is not leaking) D │ ↓ FragmentManager$$ExternalSyntheticLambda0.f$0 D ├─ androidx.fragment.app.FragmentManagerImpl instance D │ Leaking: NO (GamesFragment↓ is not leaking) D │ ↓ FragmentManager.mParent D ├─ xxx.xxx.xxxx.fragment.GamesFragment instance D │ Leaking: NO (Fragment#mFragmentManager is not null) D │ ↓ Fragment.mSavedStateRegistryController D │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ D ├─ androidx.savedstate.SavedStateRegistryController instance D │ Leaking: UNKNOWN D │ Retaining 17 B in 1 objects D │ ↓ SavedStateRegistryController.savedStateRegistry D │ ~~~~~~~~~~~~~~~~~~ D ├─ androidx.savedstate.SavedStateRegistry instance D │ Leaking: UNKNOWN D │ Retaining 494 B in 18 objects D │ ↓ SavedStateRegistry.components D │ ~~~~~~~~~~ D ├─ androidx.arch.core.internal.SafeIterableMap instance D │ Leaking: UNKNOWN D │ Retaining 471 B in 17 objects D │ ↓ SafeIterableMap["androidx.lifecycle.internal.SavedStateHandlesProvider"] D │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ D ├─ androidx.lifecycle.SavedStateHandlesProvider instance D │ Leaking: UNKNOWN D │ Retaining 251 B in 10 objects D │ ↓ SavedStateHandlesProvider.viewModel$delegate D │ ~~~~~~~~~~~~~~~~~~ D ├─ kotlin.SynchronizedLazyImpl instance D │ Leaking: UNKNOWN D │ Retaining 230 B in 9 objects D │ ↓ SynchronizedLazyImpl._value D │ ~~~~~~ D ╰→ androidx.lifecycle.SavedStateHandlesVM instance D Leaking: YES (ObjectWatcher was watching this because androidx.lifecycle.SavedStateHandlesVM received D ViewModel#onCleared() callback) D Retaining 210 B in 8 objects D key = 292612b7-4d49-4a5a-a898-b2eedd194531 D watchDurationMillis = 16225 D retainedDurationMillis = 11225 D ====================================
相关代码
页面导航代码
private fun loadFragment(fragment: Fragment){ val transaction = supportFragmentManager.beginTransaction() transaction.replace(R.id.fragmentView,fragment) transaction.addToBackStack(null) transaction.commit() }
GamesFragment代码
class GamesFragment : DaggerFragment() { //Backend @Inject lateinit var gamesDealsViewModelFactory: GamesDealsViewModelFactory private var _gamesDealsViewModel: GamesDealsViewModel? = null private val gamesDealsViewModel: GamesDealsViewModel get() = _gamesDealsViewModel!! //RecyclerView private var gamesRecyclerArray: ArrayList<GamesItem> = ArrayList() private var gamesRecyclerAdapter: GamesRecyclerAdapter? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _view = DataBindingUtil.inflate(inflater,R.layout.fragment_games,container,false) init() setUpListeners() setUpObservers() getStoresList() return view.root } private fun init(){ _gamesDealsViewModel = ViewModelProvider(this@GamesFragment,gamesDealsViewModelFactory)[GamesDealsViewModel::class.java] } private fun setUpListeners(){ view.resultsRecyclerViewLayout.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) // 原有逻辑 } }) } private fun setUpObservers(){ gamesDealsViewModel.requestStatus.observe(viewLifecycleOwner){ if (it == "SUCCESS"){ // 原有逻辑 }else if(it != ""){ // 原有逻辑 } } } private fun setUpGamesRecycler(games: Games) { //Data Handel for (game in games){ gamesRecyclerArray.add(game) } //Adapter Setup gamesRecyclerAdapter = GamesRecyclerAdapter(gamesRecyclerArray,requireActivity()) //Adapter OnClick listener gamesRecyclerAdapter?.onItemClick = { game -> // 原有逻辑 } } override fun onDestroyView() { view.gamesBannerAd.destroy() gamesRecyclerAdapter = null view.resultsRecyclerViewLayout.adapter = null mInterstitialAd = null _view = null _gamesDealsViewModel = null super.onDestroyView() } }
ViewModel代码
class GamesDealsViewModel(private val gamesDealsRepository: GameDealsRepository): ViewModel(){ private val coroutineExceptionHandler = CoroutineExceptionHandler{ _, t -> run { requestStatus.postValue(t.message.toString()) } } fun startGamesRequest(){ viewModelScope.launch(Dispatchers.IO+coroutineExceptionHandler) { gamesDealsRepository.getGamesDeals() } } val gamesDealsData :LiveData<Games> get() = gamesDealsRepository.games val requestStatus: MutableLiveData<String> get() = gamesDealsRepository.requestStatus }
问题分析与解决方法
从泄漏日志的引用链可以看出,最终泄漏的是SavedStateHandlesVM(已收到onCleared()回调但仍被持有),引用链从Native层广告WebView延伸到MainActivity,再关联到Fragment的SavedState相关组件。以下是针对性的排查和修复步骤:
1. 移除RecyclerView滚动监听的强引用
匿名内部类形式的OnScrollListener会持有Fragment的强引用,且无法被自动移除。需将其改为成员变量,在Fragment销毁时手动移除:
class GamesFragment : DaggerFragment() { // 添加成员变量存储滚动监听 private var scrollListener: RecyclerView.OnScrollListener? = null private fun setUpListeners(){ scrollListener = object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) // 原有逻辑 } } view.resultsRecyclerViewLayout.addOnScrollListener(scrollListener!!) } override fun onDestroyView() { // 移除滚动监听 scrollListener?.let { view.resultsRecyclerViewLayout.removeOnScrollListener(it) } scrollListener = null // 其他销毁逻辑 } }
2. 优化Adapter的Context引用
传递requireActivity()会让Adapter持有Activity的强引用,改为使用Fragment的context,并在Adapter中用弱引用持有:
// Fragment中修改Adapter初始化 gamesRecyclerAdapter = GamesRecyclerAdapter(gamesRecyclerArray, viewLifecycleOwner.context) // Adapter类修改 class GamesRecyclerAdapter(private val gamesList: List<GamesItem>, context: Context) : RecyclerView.Adapter<...>() { private val contextRef = WeakReference(context) // 使用时通过弱引用获取 private fun getContext(): Context? = contextRef.get() // 其他逻辑 }
3. 彻底清理广告组件
确保所有广告实例都被正确销毁,而不仅仅是置null:
override fun onDestroyView() { view.gamesBannerAd.destroy() mInterstitialAd?.destroy() // 主动销毁插屏广告 mInterstitialAd = null // 其他销毁逻辑 }
4. 清理Fragment成员变量
在onDestroyView中清空数据集,避免持有大量数据对象的引用:
override fun onDestroyView() { gamesRecyclerArray.clear() gamesRecyclerAdapter?.onItemClick = null // 清空点击监听 // 其他销毁逻辑 }
5. 移除不必要的ViewModel手动置null
ViewModel由ViewModelProvider管理,生命周期与Fragment绑定,无需手动置null,移除_gamesDealsViewModel = null这行代码。
内容的提问来源于stack exchange,提问作者YOUSEF -ELSAYED
相关产品推荐
相关产品推荐

