前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ViewPager与Fragment那些事儿

ViewPager与Fragment那些事儿

作者头像
MelonTeam
发布2018-01-04 16:12:50
8270
发布2018-01-04 16:12:50
举报
文章被收录于专栏:MelonTeam专栏MelonTeam专栏

本文会讲解:

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,有兴趣的同学可以一起探讨一下。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一:背景
  • 二:问题
  • 三:回顾与再优化
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档