【问题标题】:Fragment leaking in ViewPager2 with FragmentStateAdapter on onDestroyView()在 onDestroyView() 上使用 FragmentStateAdapter 在 ViewPager2 中泄漏的片段
【发布时间】:2021-04-26 00:11:42
【问题描述】:

仅供参考:问题很简短,但以防万一我在最后添加了更多可能相关的信息。

我需要一个无限滚动的 ViewPager2,我想重用项目中的 Fragment,因为它已经设计好并且调用已经很好地使用它的 viewLifeCycle.. 此外,我知道 VP 会回收屏幕外的 Fragments(显示的 Fragment 的 1 个偏移位置),并且在任何给定时刻至少有多达 3 个 Fragments,因此选择使用 Fragments。

问题在于,当转到第四页时,ViewPager2 尝试删除第一个 Fragment(如预期的那样),LeakCanary 向我显示了这一点(最后是整个诊断。):

D/LeakCanary: Watching instance of androidx.core.widget.NestedScrollView (com.****.****.ui.***.pages.add_element.SearchPageFragment2 received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 294cd9eb-3d6a-4c98-a69c-5d20e4c1652f

诊断从不指向我的引用,只指向 android 库引用。

在下面的代码之前,我有更多的行,但我一直在修剪它们,直到保持在最低限度并且泄漏仍然存在。

// ----- onViewCreated() ------

MyPagerAdapter mPa = new MyPagerAdapter(
                getChildFragmentManager(),
                getViewLifecycleOwner().getLifecycle()
        );

vp.setAdapter(mPa);

MyPagerAdapter.class 扩展 FragmentStateAdapter:

 @NonNull
    @Override
    public Fragment createFragment(int position) {
        return new SearchPageFragment2();    //Test Fragment
    }

@Override
    public int getItemCount() {
        return 8;    //Test fixed number
    }

泄漏的片段:

public class SearchPageFragment2 extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return FragmentSearchPageBinding.inflate(inflater).getRoot();
    }
}

是什么导致了内存泄漏??

问题到此结束。

序言...

主视图(泄漏发生时显示的最远的 Fragment 祖先)是我们从 Home Fragment 导航到的 BackStackEntry,此视图包含一个工具栏,其中包含有关应用程序的主要信息,下面的工具栏是主要的这个视图的内容,一个带有 3 个片段的固定大小的 ViewPager2,在第一个片段上......我创建了一个“MutableFrameLayout”:

private final MutableFrameLayoutAdapter<ElementDBLoaderViewModel.FrameActions> adapter = new MutableFrameLayoutAdapter<>(
            this,    //Fragment owner
            this::getChildFragmentManager,    //FragmentManager supplier
            () -> ElementDBLoaderViewModel.FrameActions.initiating,    //initialValue
            action -> {    //Function<X, Fragment>
                switch (action) {
                    case crossed:
                        return new AddElementExpandedFragment();
                    case not_crossed:
                        return new AddElementFragment();
                    case explore:
                        return new MainDBPaginationFragment2();
                }
                return null;
            }
    );

这样就可以这样使用了:

binding.fragmentContainer.setAdapter(adapter);
adapter.changeContent(ElementDBLoaderViewModel.FrameActions.crossed)

该组件是防漏的,在不同情况下经过数小时的测试。

这个组件的主要“引擎”:

......

if (oldFragment != null) {

                    FragmentTransaction ft = stackFm.beginTransaction();

                    ft.remove(oldFragment);


                    addCommit(ft, newFragment);
                }

其中 "stackFm" 是在构造函数中使用 childFragmentManager.get() ("this::getChildFragmentManager") 获取 FragmentManager 供应商的结果

......

private void addCommit(FragmentTransaction ft, Fragment newFragment) {
        fragmentCreated.get().fragmentCreated(newFragment);    //stateless adapter interface reference
        ft.add(getId(), newFragment);
        ft.commit();
    }

......

我们的想法是让组件易于使用,没有什么花哨且直接的。

基本上这个 MutableFrameLayout 放置的固定大小的 ViewPager2 的第一页(Fragment)可以采用 3 个不同的 Fragment 的形式(取决于 DB 大小)。

泄漏的 ViewPager2 位于 MainDBPaginationFragment2.class 内部,但在到达 MainDBPaginationFragment2 片段之前,我们必须先通过 AddElementExpandedFragment.class

泄漏诊断(没有参考文献是我的)

┬───
│ GC Root: System class
│
├─ android.view.WindowManagerGlobal class
│    Leaking: NO (DecorView↓ is not leaking and a class is never leaking)
│    ↓ static WindowManagerGlobal.sDefaultWindowManager
├─ android.view.WindowManagerGlobal instance
│    Leaking: NO (DecorView↓ is not leaking)
│    ↓ WindowManagerGlobal.mViews
├─ java.util.ArrayList instance
│    Leaking: NO (DecorView↓ is not leaking)
│    ↓ ArrayList.elementData
├─ java.lang.Object[] array
│    Leaking: NO (DecorView↓ is not leaking)
│    ↓ Object[].[0]
├─ com.android.internal.policy.DecorView instance
│    Leaking: NO (ViewPager2$RecyclerViewImpl↓ is not leaking and View attached)
│    View is part of a window view hierarchy
│    View.mAttachInfo is not null (view attached)
│    View.mWindowAttachCount = 1
│    mContext instance of com.android.internal.policy.DecorContext, wrapping
│    activity com.****.****.ui.MainActivity with mDestroyed = false
│    ↓ DecorView.mAttachInfo
├─ android.view.View$AttachInfo instance
│    Leaking: NO (ViewPager2$RecyclerViewImpl↓ is not leaking)
│    ↓ View$AttachInfo.mScrollContainers
├─ java.util.ArrayList instance
│    Leaking: NO (ViewPager2$RecyclerViewImpl↓ is not leaking)
│    ↓ ArrayList.elementData
├─ java.lang.Object[] array
│    Leaking: NO (ViewPager2$RecyclerViewImpl↓ is not leaking)
│    ↓ Object[].[2]
├─ androidx.viewpager2.widget.ViewPager2$RecyclerViewImpl instance
│    Leaking: NO (View attached)
│    View is part of a window view hierarchy
│    View.mAttachInfo is not null (view attached)
│    View.mID = R.id.null
│    View.mWindowAttachCount = 1
│    mContext instance of com.****.****.ui.MainActivity with
│    mDestroyed = false
│    ↓ ViewPager2$RecyclerViewImpl.mRecycler
│                                  ~~~
├─ androidx.recyclerview.widget.RecyclerView$Recycler instance
│    Leaking: UNKNOWN
│    Retaining 48453 bytes in 442 objects
│    ↓ RecyclerView$Recycler.mRecyclerPool
│                            ~~~~~
├─ androidx.recyclerview.widget.RecyclerView$RecycledViewPool instance
│    Leaking: UNKNOWN
│    Retaining 46192 bytes in 424 objects
│    ↓ RecyclerView$RecycledViewPool.mScrap
│                                    ~~
├─ android.util.SparseArray instance
│    Leaking: UNKNOWN
│    Retaining 46176 bytes in 423 objects
│    ↓ SparseArray.mValues
│                  ~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    Retaining 46111 bytes in 421 objects
│    ↓ Object[].[0]
│               ~
├─ androidx.recyclerview.widget.RecyclerView$RecycledViewPool$ScrapData instance
│    Leaking: UNKNOWN
│    Retaining 46067 bytes in 420 objects
│    ↓ RecyclerView$RecycledViewPool$ScrapData.mScrapHeap
│                                              ~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    Retaining 46035 bytes in 419 objects
│    ↓ ArrayList.elementData
│                ~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    Retaining 46015 bytes in 418 objects
│    ↓ Object[].[0]
│               ~
├─ androidx.viewpager2.adapter.FragmentViewHolder instance
│    Leaking: UNKNOWN
│    Retaining 43762 bytes in 400 objects
│    ↓ FragmentViewHolder.itemView
│                         ~~~~
├─ android.widget.FrameLayout instance
│    Leaking: UNKNOWN
│    Retaining 43677 bytes in 399 objects
│    View not part of a window view hierarchy
│    View.mAttachInfo is null (view detached)
│    View.mID = R.id.null
│    View.mWindowAttachCount = 1
│    mContext instance of com.****.****.ui.MainActivity with
│    mDestroyed = false
│    ↓ FrameLayout.mMatchParentChildren
│                  ~~~~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    Retaining 41573 bytes in 385 objects
│    ↓ ArrayList.elementData
│                ~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    Retaining 41553 bytes in 384 objects
│    ↓ Object[].[0]
│               ~
╰→ androidx.core.widget.NestedScrollView instance
​     Leaking: YES (ObjectWatcher was watching this because com.****.
​     ****.ui.****.pages.add_element.SearchPageFragment2
​     received Fragment#onDestroyView() callback (references to its views
​     should be cleared to prevent leaks))
​     Retaining 41549 bytes in 383 objects
​     key = 294cd9eb-3d6a-4c98-a69c-5d20e4c1652f
​     watchDurationMillis = 7979
​     retainedDurationMillis = 2978
​     View not part of a window view hierarchy
​     View.mAttachInfo is null (view detached)
​     View.mID = R.id.scrolling_content_table
​     View.mWindowAttachCount = 1
​     mContext instance of com.****.****.ui.MainActivity with
​     mDestroyed = false

【问题讨论】:

    标签: android-fragments view memory-leaks android-viewpager2 android-ondestroy


    【解决方案1】:

    至少对我来说解决方案是停止使用 FragmentStateAdapter,而使用 RecyclerViewAdapter,并向这个适配器提交一个整数列表,表示每个索引的索引页面(数据库实时连接所需),同时确定页面批次的大小(由初始数据库请求给出)。

    最后,在每个 viewHolder 的视图创建中,每个页面的内容都被提供,包括已解析的视图模型内容。

    解决方案的一个重要且最困难的部分是授予每个 viewHolder 自己的生命周期,以便每个 viewHolder 都可以自行管理其数据库连接。

    您可以猜到代码太可怕了,即使我着迷地尝试尽我所能订购它,它仍然没有充分组织 IMO。

    我对此的解释是,自己进行分页绝对是噩梦……但有可能。

    还封装了页面控件、数据库请求和显示等分页功能,使得分页的整个“概念”本身成为一个单独的单元,使得将每个组件单独封装起来绝对困难每个组件的功能都与其消费者紧密相连。

    一个例子是您的数据库查询假定“枢轴点”的方式(无论它们是否具有包容性,或者甚至根本不需要它们,您可以随机跳转到您想要的任何页面(索引)),我是对其他数据库不够熟悉,但如果这在服务之间发生了足够的变化,甚至可能需要改变整个分页的工作方式,自下而上。

    绝对可以封装的组件是:适配器,数据的“滚动器”,它将在定义页面控制功能时定义页面数量和页面位置,以及一个视图模型,它将简单地用作滚动条本身,仅此而已。实际的展示需要以最野蛮的方式与所有这些组件交织在一起。并且您的滚动条必须有多个输入/输出源(数据库输入:本地和远程,用户输入(页面控制器),图形输出:页面和页面控制器)

    【讨论】:

      猜你喜欢
      • 2019-08-03
      • 2021-01-12
      • 2020-01-16
      • 1970-01-01
      • 2020-06-12
      • 1970-01-01
      • 2020-04-16
      • 2020-09-21
      • 2020-11-01
      相关资源
      最近更新 更多