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

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

问题描述

起初,我在 BottomNavigationView 和数据绑定的选项卡中遇到了 ViewPager2 的问题,数据绑定也随 ViewPager2 泄漏,应该在 onDestroyView 中为空,泄漏并设法将问题缩小到 ViewPager2 ,同时使用 findNavController()从包含ViewPager2的片段导航到另一个片段.导航.

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.

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

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

这是代码

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>

包含 ViewPager2 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>

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

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>

从Leak Canary堆放垃圾

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

我还添加了 github链接希望自己检查一下或重新创建问题.

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

推荐答案

从片段的 onDestroyView 方法中的ViewPager2删除适配器,使用 FragmentStateAdapter

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()
 }

还要在片段的 onDestroyView 中将数据绑定设置为null,我在基本片段中进行了设置,这导致了与数据绑定相关的内存泄漏.或按照此处所述将其用于viewBinding,它适用到数据绑定.

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
} 

注意:片段超出了它们的视图.确保您清理任何片段中对绑定类实例的引用onDestroyView()方法.

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

防止片段中使用 ViewPager2 导致内存泄漏的另一件事是使用 viewLifeCycleOwner 的lifeCycle,该生命周期介于 onCreateView 之间在此处中,使用FragmentStateAdapter代替onDestroyView 代替 this .

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在被导航组件Navigation替换它所在的片段后泄漏的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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