1

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

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

问题是,当转到第四页时,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);

扩展 FragmentStateAdapter 的 MyPagerAdapter.class:

 @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 祖先)是一个 BackStackEntry,我们从 Home Fragment 导航到该视图,该视图包含一个工具栏,其中包含有关应用程序的主要信息,下面的工具栏是这个的主要内容视图,一个具有 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 的第一页(片段)可以采用 3 个不同片段的形式(取决于 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
4

1 回答 1

0

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

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

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

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

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

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

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

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

于 2021-09-11T15:58:24.933 回答