ViewPager与Fragment那些事儿

本文会讲解:

1.viewPager与Fragment使用过程中,偶现页面混乱问题的可能原因以及解决方案。

2.notifyDataSetChange方法在viewPager中不起作用的问题的解决方案。

3.通过修改FragmentPagerAdapter,避免Fragment被过度持有。

4.探讨viewPager中mOffscreenPageLimit的作用。

一:背景

最近开发一个需求,页面UI大致如下:

要求每一个tab都可以对应一个全新的子页面。很自然的想到使用ViewPager+Fragment这一对组合,其中Fragment复用于子页面和主页面中的tab内容。

在开发之前,考虑了产品需求和用户实用场景:

1.产品需求:输入框只要有变化,就会以输入框当前词触发本地搜索,并且依据本地搜索元素数量来判断是否自动触发网络搜索。

当触发网络搜索有回包之后,会出现上方的tabHost。下方内容区域展示可滑动。tabHost可点击。

2.用户场景:用户可能会在这个页面输入很多词,可能用户输入的过程是”王” -> “王者” -> “王者荣耀”。可能用户刚刚搜”王”,没来得及输入完”王者荣耀”四个字,就已经触发了网络搜索,并且产生回包,展示tabHost。

考虑到两个问题之后,认为需要对Fragment做重用处理,如果在搜第一个字的时候产生多个Fragment,那么搜王者荣耀的时候,应该能够复用第一次产生的Fragment,否则可能会导致new 过多的Fragment对象,导致性能问题。

首先我对要进行复用的Fragment建立了一个软引用缓存:

备为后续重用Fragment时取用的容器。

当无缓存时,才会去重新new一个。否则只是对Fragment中必要的参数重新设置即可。

二:问题

需求开发阶段,自测时经常发生页面错乱的问题,类似这样:

这可是严重问题,必须解决!于是开始了漫长的定位过程~

先思考复现场景,由于采用复用+缓存的策略,可能在当前页面展示音乐tab内容的Fragment,在上一次搜索中被用来展示兴趣部落tab内容。

此处省略走查复用Fragment时代码逻辑是否有问题花费的10000000ms…..

Orz。。。

在走查复用代码,发现没有证据表明会导致此问题之后,只能去看组件源码来排查问题,首先想到的是adapter获取Fragment的时候是否有什么特别的处理。

考虑自定义的adapter中取item的方法:

代码比较简单,看不出问题所在。

于是考虑adapter在什么情况下会调用getItem方法,通过阅读源码得知:

只有在mFragmentManager.findFragmentByTag(name)不为空的时候,才会走到我们的getItem逻辑。

那么什么时候findFragmentByTag不为空?经过验证,只要对应name的Fragment之前已经被加入过mFragmentManager,即调用了图中方法

并且没有调用remove方法,后续mFragment都是可以从其中获取到对应name的Fragment的。

由于从mFragmentManager取Fragment是依据makeFragmentName方法来的,传入的参数有container.getId()和itemId。

其中container.getId()固定为一个默认值,于是去看getItemId的具体实现:

很简单,只是传回一个position,但是问题就来了:

用户在第一次搜索回包,建立FragmentList,此时Fragment的itemId和Fragment展示内容的关系可能是这样

而第二次搜索回包时,后台要求的顺序未必按照音乐,电影,部落来。经过重用之后,可能变成这样:

这个时候如果在instantiateItem方法中还去用position去获取对应的Fragment,这里可能导致新取出来的用来展示”电影”tab的Fragment,实际上是之前用来展示”音乐”tab的Fragment。

听起来很有道理,似乎解释了为什么页面会展示错乱的问题,话不多说,立刻修改了getItemId方法。

新的Id已经和展示内容绑定起来了,但….

问题并没有解决orz。。。。

于是通过不断打log以及利用搜索引擎,想找到一点蛛丝马迹,倒是搜到了一些反映FragmentPagerAdapter的notifyDataSetChange不生效的问题:

有人说只要在getItemPostion方法的实现中返回这个值,就可以保证notifyDataSetChange生效。 public int getItemPosition(Object object) { return PagerAdapter.POSITION_NONE; }

看起来不生效这个问题很严重啊,在自己的代码中也多处使用到adapter的notifyDatasetChange方法,会不会也有这个所谓不生效的问题?

那么为什么返回这个参数能保证数据集正确更新到?看看源码咯:

这里可以发现,当返回的postion为NONE时,mItems会remove掉对应位置保存的item,同时也会通知adapter调用destroyItem方法,其中传入的第三个参数ii.object就是我们的Fragment对象。

问题来了:为什么一定要传POSITION_NONE,传别的不行吗,这个方法不应该是只为返回NONE来设计的吧,不然要他何用。继续看源码~

当我传入一个>0的数,会走到这里的逻辑,也就是简单的进行赋值操作。

随后会调用sort方法进行排序,并走进这里的判断,辗转调用到populate方法。

在populate方法中,如果当前位置的item找不到,则会调用addNewItem方法,其中会调用adapter的instantiateItem方法,来重新”生成”一个Fragment。

由此可见,所谓notifyDataSetChange不生效的原因,并不是一定要在getItemPostion中返回POSITION_NONE,而是要为每一个Fragment赋予正确的位置。

当组件发现在当前要展示的页面找不到对应位置的Fragment的时候,自然会调用addNewItem方法,产生一个新的Fragment对象。

所以正确的修改方式如下:

由于fragments的顺序和我们的tab展示的顺序是一致的,所以只要把object在fragments中的位置传递回去即可,如果object的位置不在list中,就可以return POSTION_NONE,通知组件删除啦~

经过这样的修改,发现问题迎刃而解~

三:回顾与再优化

1.软引用缓存失效问题

其实从检查instantiateItem方法的时候,我们发现adapter已经为我们的fragment对象创建了引用,保存了下来,这样会使得文头一开始提到的软引用缓存策略失效。

这里如何改动呢,方法其实很简单,通过观察DatasetChange相关的代码,我们发现当item返回的postion为NONE时,mItems会remove掉对应位置保存的item,同时也会通知adapter调用destroyItem方法。

观察adapter的默认destroyItem实现:

仅仅是做了detach操作,这还不够,于是我改了一行,变成了

同样的,在instantiateItem方法里的

都只会返回null了,因为当destroyItem后需要重新instantiateItem时,已经没有保存在mFragmentManager的fragment对象了~ 事实上我们重新去getItem的成本也很低,只是去从list集合中取了一个对象而已。

2.Fragment自动预加载问题:

在查看DatasetChange的代码时,发现一个很有意思的方法和常量

通过查看注释和调试,发现他是用来控制展示一个fragment之后,自动预加载两边fragment的数量,默认和最小值都为1。

问题来了,为什么不能为0? 因为之前看到微码上有人分享了一个在这种viewpager场景下懒加载fragment的代码,会想到为什么不在这个地方对组件进行微调,以达到每次都只加载一个fragment的效果?

于是debug进行调试,强行将mOffscreenPageLimit赋值为0,发现并没有生效~

查看代码发现主要在这里出了问题:

首先根据mOffscreenPageLimit计算startPos的值。

然后查看这部分循环,针对mCurItem左边做处理的代码

这部分是对viewPager当前展示页面左边数据内容进行处理的代码,可以看到extraWidthLeft被赋初值为0。

在第4行,leftWidthNeeded被赋值,其中curItem.widthFactor的默认赋值为1,故for循环中第一次循环中,在第7行的判断分支无法满足。

又因为我们考虑的是懒加载,只考虑只加载自己当前展示页面的fragment,故第三行ii赋值必然取不到数据,为null。

最后会走进26行的分支里面,调用addNewItem方法,生成的位置正好就是第一次循环时pos的值,即当前页面左边的页面fragment。

直到下一次循环,才会走进前两个分支。

目前还不清楚这里为什么有这样的设计,暂时也没有去动手对viewpager进行改造,使其支持每次只加载一个fragment,有兴趣的同学可以一起探讨一下。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏pangguoming

Maven WEB 项目使用ProGuard进行混淆,最佳解决方案

近期公司的Android项目做了混淆,虽说对于保护代码并不是100%的,但混淆后的代码可以使那些不法份子难以阅读,这样也能对代码的保护做出贡献。  于是,公司写...

1811
来自专栏.NET开发者社区

C#Winform使用扩展方法自定义富文本框(RichTextBox)字体颜色

在利用C#开发Winform应用程序的时候,我们有可能使用RichTextBox来实现实时显示应用程序日志的功能,日志又分为:一般消息,警告提示 和错误等类别。...

2276
来自专栏IMWeb前端团队

React函数式进阶

React让很多人让追捧的一个特性是它的所有的组件都是完全由JavaScript组成的。组件的定义是JavaScript,组件的模板也可以是JavaScript...

2146
来自专栏前端知识分享

Angular中通过$location获取地址栏的参数详解

  最近,项目开发正在进行时,心有点燥,许多东西没来得及去研究,今天正想问题呢,同事问到如何获取url中的参数,我一时半会还真没想起来,刚刚特意研究了一下,常用...

643
来自专栏tkokof 的技术,小趣及杂念

NGUI AnchorPoint与Camera CullingMask的结合之痛

NGUI内建的Anchor系统可以方便的定位UI(底层的一个支持结构是AnchorPoint),例如一些需要全屏显示的游戏界面一般都需要借助这项功能;另外的,对...

612
来自专栏三丰SanFeng

Linux时间时区详解与常用时间函数

时间与时区 整个地球分为二十四时区,每个时区都有自己的本地时间。 Ø UTC时间 与 GMT时间 我们可以认为格林威治时间就是时间协调时间(GMT = UTC)...

2006
来自专栏我杨某人的青春满是悔恨

Swift2网络操作和异常处理

相信写过Swift的人应该都知道Alamofire,它是AFNetworking的Swift版本,同一个作者写的。之前在项目中我也一直使用Alamofire,但...

691
来自专栏kevindroid

NDK学习笔记(1)——第一个jni程序

1004
来自专栏Flutter入门

Flutter入门三部曲(2) - 界面开发基础

上一节我们熟悉了初始化后的flutter的界面。这一节,我们就来重点了解一下这部分的内容。

970
来自专栏有趣的django

微信小程序入门(四)

WXSS(WeiXin Style Sheets)是一套样式语言,用于WXML的组件样式

1172

扫码关注云+社区