通过导航组件导航替换其所在的片段后,片段中的 ViewPager2 泄漏 [英] ViewPager2 inside a fragment leaks after replacing the fragment it's in by Navigation Component navigate

本文介绍了通过导航组件导航替换其所在的片段后,片段中的 ViewPager2 泄漏的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

起初,我在 BottomNavigationView 和数据绑定的选项卡内遇到了 ViewPager2 的问题,数据绑定也会随着 ViewPager2 泄漏,应该在 onDestroyView 中被清零,泄漏并设法将问题缩小到 ViewPager2,同时使用 findNavController().navigate.

这是如何发生的,当我导航到另一个用 ViewPager2 替换当前片段时会发生这种情况.

这是代码

class MainActivity : AppCompatActivity() {覆盖 fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)}}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto";android:layout_width=match_parent"android:layout_height=match_parent"><androidx.fragment.app.FragmentContainerViewandroid:id="@+id/nav_host_fragment";android:name="androidx.navigation.fragment.NavHostFragment";android:layout_width=match_parent"android:layout_height=match_parent"app:defaultNavHost="true";app:layout_constraintBottom_toBottomOf=父级"app:layout_constraintLeft_toLeftOf=父"app:layout_constraintRight_toRightOf=父"app:layout_constraintTop_toTopOf=父级"app:navGraph="@navigation/nav_graph";/></androidx.constraintlayout.widget.ConstraintLayout>

nav_graph.xml

<导航 xmlns:android="http://schemas.android.com/apk/res/android";xmlns:app="http://schemas.android.com/apk/res-auto";xmlns:tools="http://schemas.android.com/tools"android:id="@+id/nav_graph_parent";app:startDestination="@id/parent_dest"><片段android:id="@+id/parent_dest";android:name=com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.viewpagerfragment.ViewPagerContainerFragment"android:label="MainFragment";工具:布局=@layout/fragment_viewpager_container"><!-- 登录--><动作android:id=@+id/action_main_dest_to_loginFragment2"app:destination="@id/loginFragment2";/></片段><!-- 登录--><片段android:id="@+id/loginFragment2";android:name=com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.blankfragment.LoginFragment2"android:label="LoginFragment2";工具:布局=@layout/fragment_login2"/></导航>

包含 ViewPager2TabLayout

的片段

class ViewPagerContainerFragment : Fragment() {覆盖乐趣 onCreateView(充气器:LayoutInflater,容器:ViewGroup?,已保存的实例状态:捆绑?): 看法?{返回 inflater.inflate(R.layout.fragment_viewpager_container,容器,假)}覆盖 fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(视图,savedInstanceState)//ViewPager2val viewPager = view.findViewById(R.id.viewPager)/*使用此片段在此片段内为 ViewPager 设置适配器,更具体地说,childFragmentManager 作为参数*/viewPager.adapter = ChildFragmentStateAdapter(this)//选项卡布局val tabLayout = view.findViewById(R.id.tabLayout)//绑定标签和viewpagerTabLayoutMediator(tabLayout, viewPager) { tab, position ->当(位置){0 ->tab.text =首页"1 ->tab.text = "仪表板";2 ->tab.text = "通知";3 ->tab.text = "登录";}}.附()}}

fragment_viewpager_container

<com.google.android.material.tabs.TabLayoutandroid:id="@+id/tabLayout";android:layout_width=match_parent"android:layout_height="wrap_content";app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf=父级"app:tabMode="scrollable";/><androidx.viewpager2.widget.ViewPager2android:id="@+id/viewPager";android:layout_width=match_parent"android:layout_height=0dp"app:layout_constraintBottom_toBottomOf=父级"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf=@+id/tabLayout";/></androidx.constraintlayout.widget.ConstraintLayout>

片段没有什么特别之处,但我添加了其中一种布局,也许 Material 小部件正在泄漏,我不知道

<com.google.android.material.textview.MaterialTextViewandroid:id="@+id/tvTitle";android:layout_width="wrap_content";android:layout_height="wrap_content";android:text="Home Fragment1";android:textColor="#fff";android:textSize="32sp";app:layout_constraintBottom_toBottomOf=父级"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizo​​ntal_bias=0.5";app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf=父级"app:layout_constraintVertical_bias=0.3";/><com.google.android.material.button.MaterialButtonandroid:id="@+id/btnNextPage";android:layout_width=match_parent"android:layout_height="wrap_content";android:text="下一页"app:layout_constraintBottom_toBottomOf=父级"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf=@+id/tvTitle"/></androidx.constraintlayout.widget.ConstraintLayout>

来自 Leak Canary 的堆转储

┬───│ GC Root:系统类│├─ android.app.ActivityThread 类│ Leaking: NO(MainActivity↓不泄漏,一个类从不泄漏)│ ↓ 静态ActivityThread.sCurrentActivityThread├─ android.app.ActivityThread 实例│ 泄漏:NO(MainActivity↓ 不泄漏)│ ↓ ActivityThread.mTopActivityClient├─ android.app.ActivityThread$ActivityClientRecord 实例│ 泄漏:NO(MainActivity↓ 不泄漏)│ ↓ ActivityThread$ActivityClientRecord.activity├─ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.MainActivity 实例│ 泄漏:NO(NavHostFragment↓ 不泄漏,Activity#mDestroyed 为假)│ ↓ MainActivity.mFragments├─ androidx.fragment.app.FragmentController 实例│ 泄漏:NO(NavHostFragment↓ 不泄漏)│ ↓ FragmentController.mHost├─ androidx.fragment.app.FragmentActivity$HostCallbacks 实例│ 泄漏:NO(NavHostFragment↓ 不泄漏)│ ↓ FragmentActivity$HostCallbacks.mFragmentManager├─ androidx.fragment.app.FragmentManagerImpl 实例│ 泄漏:NO(NavHostFragment↓ 不泄漏)│ ↓ FragmentManagerImpl.mPrimaryNav├─ androidx.navigation.fragment.NavHostFragment 实例│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏且Fragment#mFragmentManager 不为空)│ ↓ NavHostFragment.mChildFragmentManager├─ androidx.fragment.app.FragmentManagerImpl 实例│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏)│ ↓ FragmentManagerImpl.mFragmentStore├─ androidx.fragment.app.FragmentStore 实例│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏)│ ↓ FragmentStore.mActive├─ java.util.HashMap 实例│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏)│ ↓ HashMap.table├─ java.util.HashMap$Node[] 数组│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏)│ ↓ HashMap$Node[].[0]├─ java.util.HashMap$Node 实例│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏)│ ↓ HashMap$Node.value├─ androidx.fragment.app.FragmentStateManager 实例│ 泄漏:NO(ViewPagerContainerFragment↓ 不泄漏)│ ↓ FragmentStateManager.mFragment├─ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.viewpagerfragment.ViewPagerContainerFragment 实例│ 泄露:NO(Fragment#mFragmentManager 不为空)│ ↓ ViewPagerContainerFragment.mLifecycleRegistry│ ~~~~~~├─ androidx.lifecycle.LifecycleRegistry 实例│ 泄漏:未知│ ↓ LifecycleRegistry.mObserverMap│ ~~~~├─ androidx.arch.core.internal.FastSafeIterableMap 实例│ 泄漏:未知│ ↓ FastSafeIterableMap.mEnd│ ~~├─ androidx.arch.core.internal.SafeIterableMap$Entry 实例│ 泄漏:未知│ ↓ SafeIterableMap$Entry.mKey│ ~~├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3 实例│ 泄漏:未知│ 匿名类实现 androidx.lifecycle.LifecycleEventObserver│ ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3.this$1│ ~~├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer 实例│ 泄漏:未知│ ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer.mViewPager│ ~~~~├─ androidx.viewpager2.widget.ViewPager2 实例│ Leaking: YES (View detached and has parent)│ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.MainActivity 的 mContext 实例,mDestroyed = false│ 查看#mParent 已设置│ View#mAttachInfo 为空(视图分离)│ View.mID = R.id.viewPager│ View.mWindowAttachCount = 1│ ↓ ViewPager

如果您愿意,我还会添加 github 链接自己检查或重现问题.

解决方案

Removing adapter from ViewPager2 in onDestroyView fragment 方法解决了FragmentStateAdapter

的内存泄漏问题>

 覆盖 fun onDestroyView() {val viewPager2 = dataBinding?.viewPagerviewPager2?.let {it.adapter = null}super.onDestroyView()}

还在片段的 onDestroyView 中将数据绑定设置为 null,我在基础片段中进行了此操作,这导致了与数据绑定相关的内存泄漏.或者按照此处中提到的方式使用它用于viewBinding,它适用到数据绑定.

私有变量_binding:ResultProfileBinding?= 空//该属性只在 onCreateView 和//onDestroyView.私有 val 绑定 get() = _binding!!覆盖乐趣 onCreateView(充气器:LayoutInflater,容器:ViewGroup?,已保存的实例状态:捆绑?): 看法?{_binding = ResultProfileBinding.inflate(充气器,容器,假)val 视图 = binding.root返回视图}覆盖乐趣 onDestroyView() {super.onDestroyView()_binding = null}

<块引用>

注意:片段比它们的视图更持久.确保你清理任何对片段中绑定类实例的引用onDestroyView() 方法.

防止片段内 ViewPager2 内存泄漏的另一件事是使用 viewLifeCycleOwner 的生命周期,它位于 onCreateView 之间onDestroyView 而不是 this 和 FragmentStateAdapter,如此处所述.

FragmentManager fm = getChildFragmentManager();生命周期生命周期 = getViewLifecycleOwner().getLifecycle();fragmentAdapter = new FragmentAdapter(fm, 生命周期);

At first, i had the issue for ViewPager2 inside a tab of BottomNavigationView and data binding , data binding also leaks with ViewPager2 and should be nulled in onDestroyView, leaking and managed to narrow the issue to ViewPager2 while navigating from fragment that contains ViewPager2 to another fragment using findNavController().navigate.

Here is how it occurs, it occurs when i navigate to another fragment that replaces the current one with ViewPager2.

Here is the code

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"

        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

nav_graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph_parent"
    app:startDestination="@id/parent_dest">

    <fragment
        android:id="@+id/parent_dest"
        android:name="com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.viewpagerfragment.ViewPagerContainerFragment"
        android:label="MainFragment"
        tools:layout="@layout/fragment_viewpager_container">

        <!-- Login -->
        <action
            android:id="@+id/action_main_dest_to_loginFragment2"
            app:destination="@id/loginFragment2" />
    </fragment>

    <!-- Login -->
    <fragment
        android:id="@+id/loginFragment2"
        android:name="com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.blankfragment.LoginFragment2"
        android:label="LoginFragment2"
        tools:layout="@layout/fragment_login2"/>

</navigation>

Fragment that contains ViewPager2 and TabLayout

class ViewPagerContainerFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_viewpager_container, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // ViewPager2
        val viewPager = view.findViewById<ViewPager2>(R.id.viewPager)

        /*
            Set Adapter for ViewPager inside this fragment using this Fragment,
            more specifically childFragmentManager as param
         */
        viewPager.adapter = ChildFragmentStateAdapter(this)

        // TabLayout
        val tabLayout = view.findViewById<TabLayout>(R.id.tabLayout)

        // Bind tabs and viewpager
        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            when (position) {
                0 -> tab.text = "Home"
                1 -> tab.text = "Dashboard"
                2 -> tab.text = "Notification"
                3 -> tab.text = "Login"
            }
        }.attach()
    }
}

fragment_viewpager_container

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:tabMode="scrollable" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tabLayout" />

</androidx.constraintlayout.widget.ConstraintLayout>

Nothing special with fragments but i add one of the layouts, maybe Material widgets are leaking, i don't know

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorHome1"
    android:padding="8dp">

    <com.google.android.material.textview.MaterialTextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Home Fragment1"
        android:textColor="#fff"
        android:textSize="32sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.3" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnNextPage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Next Page"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvTitle" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

And heap dump from Leak Canary

┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│    ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│    Leaking: NO (MainActivity↓ is not leaking)
│    ↓ ActivityThread.mTopActivityClient
├─ android.app.ActivityThread$ActivityClientRecord instance
│    Leaking: NO (MainActivity↓ is not leaking)
│    ↓ ActivityThread$ActivityClientRecord.activity
├─ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.MainActivity instance
│    Leaking: NO (NavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│    ↓ MainActivity.mFragments
├─ androidx.fragment.app.FragmentController instance
│    Leaking: NO (NavHostFragment↓ is not leaking)
│    ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
│    Leaking: NO (NavHostFragment↓ is not leaking)
│    ↓ FragmentActivity$HostCallbacks.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│    Leaking: NO (NavHostFragment↓ is not leaking)
│    ↓ FragmentManagerImpl.mPrimaryNav
├─ androidx.navigation.fragment.NavHostFragment instance
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking and Fragment#mFragmentManager is not null)
│    ↓ NavHostFragment.mChildFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│    ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│    ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│    ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│    ↓ HashMap$Node[].[0]
├─ java.util.HashMap$Node instance
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│    ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│    Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│    ↓ FragmentStateManager.mFragment
├─ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.viewpagerfragment.ViewPagerContainerFragment instance
│    Leaking: NO (Fragment#mFragmentManager is not null)
│    ↓ ViewPagerContainerFragment.mLifecycleRegistry
│                                 ~~~~~~
├─ androidx.lifecycle.LifecycleRegistry instance
│    Leaking: UNKNOWN
│    ↓ LifecycleRegistry.mObserverMap
│                        ~~~~
├─ androidx.arch.core.internal.FastSafeIterableMap instance
│    Leaking: UNKNOWN
│    ↓ FastSafeIterableMap.mEnd
│                          ~~
├─ androidx.arch.core.internal.SafeIterableMap$Entry instance
│    Leaking: UNKNOWN
│    ↓ SafeIterableMap$Entry.mKey
│                            ~~
├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3 instance
│    Leaking: UNKNOWN
│    Anonymous class implementing androidx.lifecycle.LifecycleEventObserver
│    ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3.this$1
│                                                          ~~
├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer instance
│    Leaking: UNKNOWN
│    ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer.mViewPager
│                                                        ~~~~
├─ androidx.viewpager2.widget.ViewPager2 instance
│    Leaking: YES (View detached and has parent)
│    mContext instance of com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.MainActivity with mDestroyed = false
│    View#mParent is set
│    View#mAttachInfo is null (view detached)
│    View.mID = R.id.viewPager
│    View.mWindowAttachCount = 1
│    ↓ ViewPager

I also add github link if you wish to check for yourself or recreate the issue.

解决方案

Removing adapter from ViewPager2 in onDestroyView method of fragment solved the memory leak issue with FragmentStateAdapter

 override fun onDestroyView() {

        val viewPager2 = dataBinding?.viewPager

        viewPager2?.let {
            it.adapter = null
        }
        super.onDestroyView()
 }

Also set data binding to null in onDestroyView of fragment, i did it in base fragment, which was causing data binding related memory leak. Or use it as mentioned here for viewBinding, it applies to data binding.

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
} 

Note: Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method.

Another thing to prevent memory leaks with ViewPager2 inside a fragment is to use viewLifeCycleOwner's lifeCycle which is between onCreateView and onDestroyView instead of this with FragmentStateAdapter as mentioned here.

FragmentManager fm = getChildFragmentManager();
Lifecycle lifecycle = getViewLifecycleOwner().getLifecycle();
fragmentAdapter = new FragmentAdapter(fm, lifecycle);

这篇关于通过导航组件导航替换其所在的片段后,片段中的 ViewPager2 泄漏的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆