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 条评论
登录 后参与评论

相关文章

来自专栏葬爱家族

Mvvm、RxJava、Retrofit 三剑合璧

说起现在Android流行的app架构,脱口而出MVP、MVVM,要问两者区别,张口就来,balabalabala。。但是公司所有项目用的都是MVP,从没正式用...

972
来自专栏点滴积累

geotrellis使用(十四)导出定制的GeoTiff

Geotrellis系列文章链接地址http://www.cnblogs.com/shoufengwei/p/5619419.html 目录 前言 需求说...

2656
来自专栏更流畅、简洁的软件开发方式

【自然框架】之 “表单控件”与“实体类”

      对于简单的添加、修改,也就是没有什么业务逻辑的那种,表单控件的工作步骤是这样的,以添加数据为例。这个不用写什么代码,点点鼠标就可以搞定了。 ?   ...

2287
来自专栏xingoo, 一个梦想做发明家的程序员

【Spring实战】—— 4 Spring中bean的init和destroy方法讲解

本篇文章主要介绍了在spring中通过配置init-method和destroy-method方法来实现Bean的初始化和销毁时附加的操作。 在java中,...

1996
来自专栏前端知识分享

todomvc-app

705
来自专栏农夫安全

浅析XSS的几种测试方法

0x00 背景 最近看到一个好玩的xss社区,准备通过几个经典的关卡来剖析一下XSS,本文仅提供经典案例。 试玩链接:http://tr.secevery...

2868
来自专栏大内老A

ASP.NET MVC的View是如何被呈现出来的?[设计篇]

在前面的四篇文章中,我们介绍了各种ActionResult以及相关的请求响应机制,但是与“View的呈现”相关的ActionResult是ViewResult。...

2738
来自专栏瓜大三哥

物理约束

IO约束,如位置和IO标准 引脚分配命令 Set_property PACKAGE_PIN <pin name> [get_ports <port>] 驱动能...

1755
来自专栏学海无涯

Android开发之项目经验分享

在Android开发中,除了基本的理论知识,还需要将所学知识运用到真实的项目中,在项目中锻炼自己的分析问题、解决问题的能力,本文将总结一下本人项目中遇到的一些问...

2475
来自专栏Java3y

移动商城第四篇(商品管理)【添加商品续篇、商品审核和上下架、前台展示、后台筛选】

属性选项卡 第三个选项卡涉及到了我们的手机属性,因此,还是需要用到我们其他的数据库表:EB_FEATURE 继续做逆向工程: ? 这里写图片描述 查询出普通属性...

6459

扫码关注云+社区