You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 19:50:36