Bootstrap

FragmentStatePagerAdapter的危险

许多Android开发人员感到困惑,甚至都不知道FragmentPagerAdapterFragmentStatePagerAdapter之间的区别。也越来越notifyDatasetChanged()对工作可能是令人沮丧的时候。使用这些适配器很容易发生内存泄漏。我将从这些基础知识开始,然后更深入地介绍实现细节,并指出一些鲜为人知的陷阱(您是否知道FragmentPagerAdapter中的Fragment仅在完成Activity后才从内存中释放出来,请继续阅读:-))。

基本区别

FragmentPagerAdapter

适用于有限数量(固定)的项目(片段)。为什么?因为一旦创建,它将永远不会从FragmentManager中删除片段实例(除非Activity完成)。它仅将视图与当前不可见的片段分离。一旦无法到达您的片段,就会在片段上调用它,稍后返回该片段时,就会调用它。onDestroyView()onCreateView()

FragmentStatePagerAdapter

精明的。删除一旦你返回到现有项目片段实例重建和状态恢复此适配器适用于一个未知的数量或在项目发生很大的变化列表清单。

FragmentPagerAdapter —内存泄漏危险

FragmentPagerAdapter中的片段仅被分离,并且永远不会从FragmentManager中移除(除非活动完成)。使用FragmentPagerAdapter时,必须确保清除中对当前View或Context的所有引用onDestroyView()。否则,垃圾收集器将无法释放整个视图,甚至无法释放活动。这意味着将任何与“视图/上下文”相关的字段设置为null(Butterknife可以自动将其取消绑定),并删除所有可能泄漏“上下文”或“视图”的侦听器。

否则,可能会耗尽内存-假设FragmentPagerAdapter中有10个项目。滑动所有这些视图将在内存中保留10个视图,而不是仅保留最后三个视图(取决于setOffScreenPageLimit()设置),旋转屏幕会使其更糟(十分之七)仍将保留对“被破坏”活动的引用。

僵尸分离碎片

您还需要考虑以下问题:片段永远不会从FragmentManager中删除。内存中可能有数百个分离的实例,这是因为您要不断更改适配器绑定到的一小部分项目。它将创建新项目并保留旧的分离片段。

FragmentStatePagerAdapter的问题不大,因为它正在从FragmentManager中删除整个Fragment实例。如果您滑动许多片段,可能会遇到麻烦,并且由于每个片段都会在地图中添加一个新的Bundle实例,因此它可能会变得很大(可能会导致TransactionTooLargeException方向更改)。

带有notifyDatasetChanged()的问题

我猜我们每个人在调用notifyDatasetChanged()这些适配器之一时都会遇到问题。这样做不会刷新当前可见的片段,而您必须“来回滑动”以强制适配器重新创建当前片段。这两个PagerAdapters都在缓存和重用Fragment实例,这很好,因为否则调用notifyDataSetChanged会再次重新创建它们(即使不是必需的)。

同样非常重要的一点是,notifyDataSetChanged()适用于数据集更改的情况,这意味着是否删除或添加了某些项目。notifyDatasetChanged()方法并不用于刷新当前显示的片段或其视图。如果您希望某些数据发生更改时刷新它们的视图,则需要在Fragment中添加一些侦听器/回调。

FragmentPagerAdapter和notifyDatasetChanged()

您需要在FragmentPagerAdapter中重写两个方法以支持数据集更改。

int getItemPosition(Object object)

在宿主视图试图确定项目位置是否已更改时调用。返回POSITION_UNCHANGED如果给定项目的立场没有改变,或者POSITION_NONE如果该项目是在适配器不再存在。

默认实现假定项目永远不会改变位置并且总是返回POSITION_UNCHANGED

您需要getItemPosition(Object object)使用某种逻辑来实现,该逻辑确定当前片段在数据集中的位置(例如,迭代数据集并搜索与片段相对应的入口位置)。

始终返回POSITION_NONE会导致内存和性能低下-即使它们在数据集中的位置未更改,也会始终分离当前可见的片段并重新创建它们。并且旧的片段将保留在内存中,直到您离开活动。

long getItemId(int position)

将在给定位置返回该商品的唯一标识符。默认实现返回给定位置。如果项的位置可以更改,则子类应重写此方法。

在内部使用此方法在instantiateItem()FragmentManager中搜索现有的分离Fragment实例。调用notifyDataSetChanged()而不覆盖此方法将只返回当前索引上的现有Fragment实例。您需要返回该片段的唯一标识符

FragmentStatePagerAdapter —状态包“错误”

您只需要重写getItemPosition()即可支持FragmentStatePagerAdapter中的数据集更改(此处没有getItemId()方法)。与FragmentPagerAdapter所提到的一样,这里也有同样的事情(不一定总是返回POSITION_NONE)。

这里有一个陷阱-相当不愉快... 此博客中详细描述了该问题。问题在于FragmentStatePagerAdapter保持状态捆绑的ArrayList。如果该instantiateItem()方法不存在,方法将创建一个新的Fragment实例-但是它将查找ArrayList并根据项目索引选择Bundle!这可以是属于该索引上一个先前(不同)Fragment实例的Bundle。如果要更改数据集并更新适配器,则可以执行此操作。阅读链接的博客以获取一些可能的解决方案。

片段{}当前不在FragmentManager中

这个问题已经公开,并且四年以上没有修复!此博客对此进行了详细描述。这确实令人沮丧,在这之后,您可能会考虑拥有自己的“固定”版本的FragmentStatePagerAdapter。(编辑:我们正在研究它-将在 https://github.com/inloop/UpdatableFragmentStatePagerAdapter上提供)。

FragmentStatePagerAdapter完全早于PagerAdapters的数据集更改功能。如果您有一个需要处理数据集更改的PagerAdapter,那么与从FragmentStatePagerAdapter开始相比,直接从PagerAdapter实现合同可能会更容易。

好吧……很好,但是应该对此有一些大的警告。一个简单的解决方法是始终返回POSITION_NONE getItemPosition()。但这会带来性能上的损失。

还有PagerAdapter…

也许我应该在一开始就说过这一点-但是请记住,如果您的应用程序中有ViewPager,则不必总是使用Fragments。如果您使用一些复合的View布局和一个简单的PagerAdapter,则完全没有问题,而且也很简单。这取决于您是否需要所有生命周期回调。

;