Android中Fragment的解析和使用详解
前言
android fragment的生命周期和activity类似,实际可能会涉及到数据传递,onsaveinstancestate的状态保存,fragmentmanager的管理和transaction,切换的animation。
我们首先简单的介绍一下fragment的生命周期。
大致上,从名字就可以判断出每个生命周期是干嘛的。
appcompatactivity就是fragmentactivity的子类,如果想使用fragment,是要继承fragmentactivity,因为考虑到兼容的问题,我们要使用getsupportfragmentmanager,而这个方法是fragmentactivity中声明的。
activity中同样也有个类似的方法,getfragmentmanager,两个方法返回的都是fragmentmanager,不过一个是v4包。
至于android到底是如何为低版本兼容fragment这个问题,这里就不研究了,因为涉及到的源码估计应该很多,而且可能会很深。
fragment到底是如何将自己的生命周期和activity绑定在一起呢?
这里有一个很关键的类:fragmentcontroller。
在fragmentactivity的生命周期中,会调用fragmentcontroller对应的方法,而这些方法会调用到fragmentmanager对应的方法。
我们来看看fragmentactivity的oncreate方法。
mfragments.attachhost(null /*parent*/); super.oncreate(savedinstancestate);
这里调用了attachhost方法,而attachhost方法又调用了fragmentmanager的attachcontroller方法。
attachcontroller这个方法实际上,是将需要的fragmenthostcallback,fragmentcontainer和fragment传进来。
fragmenthostcallback是fragmentcontainer的子类,实际上,它就是fragment所要附加的activity,它持有这个activity的实例,context和handler。
fragmentcontainer和fragmenthostcallback是同一个实例,就是要附加的activity。
而fragment传入的是null,参数名是parent,这里附加的是activity,因此没有parent fragment是很正常的。
当我们使用fragmentmanager的时候,如果要添加fragment,是需要这样写:
fragmentmanager manager = ((fragmentactivity) context).getsupportfragmentmanager(); fragmenttransaction transaction = manager.begintransaction(); transaction.add(fragment, context.getclass().getsimplename()); transaction.commit();
这里出现了新的类:fragmenttransaction。
fragmenttransaction是用于处理fragment的栈操作,具体的子类是backstackrecord,它同时也是一个runnable。
当我们调用fragmenttransaction的add时候,实际上是调用backstackrecord的addop方法,op是自定义的数据结构:
static final class op { op next; op prev; int cmd; fragment fragment; int enteranim; int exitanim; int popenteranim; int popexitanim; arraylist<fragment> removed; }
也就是fragment栈里面的节点的数据结构。
当我们commit的时候,就会调用fragmentmanager的allocbackstackindex,方法内部使用了对象这是为了保证fragment的正常写入顺序,实际上,内部是用一个backstackrecord的arraylist来保存传入的backstackrecord。
执行fragment的写入后,关键一步就是调用fragmentmanager的enqueueaction,将我们的操作添加到操作队列中。
执行这个方法的时候,会先检查是否已经保存了状态,也就是是否处于onstop的生命周期,如果是的话,就会报异常信息。所以我们不能在activity的onstop里面进行任何有关fragment的操作。
为了保证操作是串行的,同样也使用了对象锁。
最关键的是运行了fragmentmanager的mexeccommit这个runnable,这里主要是把每一个active的fragment作为参数传给movetostate这个方法,判断fragment的状态。
这里的逻辑比较复杂,会将fragment的state和mcurstate进行比较。一开始commit的每个fagment的状态都是initializing。
分为2种情况:
1.mcurstate > state
说明fragment开始创建。
oncreate最后会调用fragmentcontroller和fragmentmanager的dispatchcreate,将mcurstate的状态改为created,这时同样是调用movetostate方法,每个fragment的状态都是initializing,就会开始读取保存的状态,并且分别调用fragment的onattach,oncreate,oncreateview和onviewcreate。
如果没有在commit之前就setarguments来传递数据,调用commit后是无法读取到的,因为setarguments传递过来的bundle是在fragment初始化的时候才会赋值给fragment的marguments,而fragment的初始化动作是在fragmentmanager的oncreateview中进行。我们使用fragment的时候,都是在fragmentactivity的oncreate中commit,所以这时候fragment实际上在commit的时候就会开始初始化了,如果放在commit后面setarguments,就根本没机会传递给fragment。
这里我们要注意,上面都是在fragmentactivity的oncreate中进行,也就是说,这时候activity根本还没创建好,所以关于activity的资源在这里是无法获取到的。
2.mcurstate < state
说明fragment已经创建完毕。
所以,fragment真正和activity绑定是在commit调用的时候。
官方推荐我们通过setarguments来传递构造fragment需要的参数,不推荐通过构造方法直接来传递参数,因为横竖屏切换的时候,是重新创建新的activity,也就是重新创建新的fragment,原先的数据就会全部丢失,但是setarguments传递的bundle会保留下来。
我们只要看fragmentactivity的oncreate方法就知道,它会判断之前的配置和savedinstancestate是否不为null,而savedinstancestate会保存fragment的数据,这些数据是以parcelable的形式保存下来,这些数据就是fragmentmanagerstate,如果不为null,就会重新加载这些数据。
实际上,上面的生命周期的图是有问题的,onactivitycreated真正被调用是在fragmentactivity的onstart里面,这时mcurstate就变成activity_created,而fragment的状态变成created,这时如果fragment并不是布局文件中声明 ,采用的是动态添加的方式,那么fragment就是在这里调用oncreateview和onviewcreated,并且将fragment添加到fragmentactivity的布局上。
首先我们必须明确的是,onstart的时候,activity虽然可见,但是还没有显示到前台,所以这时候才处理动态添加fragment的情况是合理的,如果我们把动态添加fragment的逻辑放在oncreate的时候,那时候activity自身的布局都还没创建,怎么可能找到container加载fragment呢?
这同时也是提醒我们,不要在fragment的oncreateview和onviewcreated处理耗时的逻辑,否则就会影响到fragmentactivity显示到前台的时间。
当fragmentactivity进入onresume的时候,已经显示到前台了,这时候发送一个消息给handler,通知fragmentmanager,mcurstate变为resumed,这时fragment就会开始进行监听事件等的设置。
当fragmentactivity进入onpause的时候,会先检查fragment是否还没有设置监听事件,如果没有,就让它进行设置,然后修改mcurstate为started,这时就属于前面的第二种情况,fragment进入onpause。
当fragmentactivity进入onstop的时候,首先通知fragmentmanager修改mcurstate为stopped,这时就会通知fragment进入onstop,然后就是handler接收到消息,通知fragmentmanager将mcurstate改为activity_created,通知fragment调用performreallystop,也就是真正的结束。
当fragmentactivity进入ondestroy的时候,会确认是否真的reallystop,然后通知fragmentmanager修改mcurstate为created,这时fragment的状态为activity_created,开始保存视图数据,调用ondestroyview,父布局开始移除fragment。
仔细看这段逻辑,就会发现,不管有没有设置fragment是需要保留的,都会进入ondetach,表示该fragment和fragmentactivity已经不再关联了。
我们再来看一下onretainnonconfigurationinstance这个方法,它会设置fragment的mretaining为true,这样就会使fragment不会进入ondestroy,就算是重新创建新的fragmentactivity,也只是清除fragment的mhost,mparentfragment,mfragmentmanager和mchildfragmentmanager,之前的数据都会保存下来,并且这个fragment并没有被销毁,这就会导致一个问题:重新创建的fragmentactivity本身也会创建新的fragment,因此会出现fragment的重叠,因为这时fragment的状态为stopped,会分别进入onstart和onresume,也就是重新显示到前台的过程。
我们在实际的测试中就会发现,在没做任何处理的情况下,fragmentmanager中的fragment是越来越多,所以实际上,考虑到这种情况:应用在后台如果被杀掉的话,重新启动应用,之前的fragment就可能会重叠在界面上。
这种情况在处理tab的时候是比较麻烦的,因为tab是好几个fragment同时显示在前台,如果activity被干掉,重新创建的时候,进入的是第一个fragment,但如果这时候是在另一个fragment下被干掉的,就可能导致这两个fragment重叠。
所以可以在oncreate中判断是否重新创建activity,只要判断savedinstancestate是否为null,如果为null,说明该activity没有被重建过,可以添加fragment,就算是上面的tab的情况也可以处理,只要不添加第一个fragment就可以。
如果是基于这样的判断来解决这个问题,我们还可以在添加fragment的时候,指定一个id或者tag,判断fragmentmanager中对应的id或者tag的fragment是否存在来决定是否要添加。
当然,如果项目实在没有需要,我们是可以强制竖屏的。
如果只是针对横竖屏切换,也有另一种解决方案,在androidmanifest中对应的activity标签中设置android:configchanges="orientation|keyboardhidden"
,但是这个属性在android 4.0以上就失效了,必须这样写才行:android:configchanges="orientation|keyboardhidden|screensize"
。这样在横竖屏切换的时候,不会走onretainnonconfigurationinstance,走的是onconfigurationchanged,切换时不会销毁当前的fragmentactivity,自然fragment也同样能够保持下来。
如果我们想要为fragment增加过场动画,针对v4和非v4,有两种做法。
1.针对v4,使用的是view animation,动画资源放在res\anim\目录下。
2.针对非v4,使用的是属性动画,动画资源放在res\animator\目录下。
一般我们使用的都是v4的fragment,并且针对的转场动画,view animation已经足够满足我们的要求。
我们再来看一下fragmenttransaction的addtobackstack这个方法。
如果我们想要实现这样的效果:点击返回键,返回的是上一个fragment。那就得调用addtobackstack这个方法。这个方法要求传入一个string的参数,实际上我们只要传入null就行,如果我们不想指定栈(虽说是栈,实际上只是个arraylist,并没有实现栈的结构)的名字。
仔细看源码,我们就会发现,如果不调用这个方法,在按返回键的时候,就直接finish当前的fragmentactivity。
fragment的回退和activity的回退是有很大的区别的,我们知道,fragment的操作是fragmenttransaction,而backstackrecord真是这些操作的具体子类实现。
这时问题就来了:如果我们是两次fragmenttransactiont添加fragment,第一次添加a,第二次添加b和c,我们回退并不是fragment,是backstackrecord的op,而op中记录的是每次操作的fragment,当我们回退第二次操作的时候,是把第二次添加的b和c都退出来。
如果我们只有一个fragment,并且也不想实现fragment的回退栈,就千万不要调用addtobackstate,不然在activity按返回键的时候,并不会马上退出activity,而是返回一个空白,因为就算是null,也会添加到backstackrecord的arraylist中,因为这个参数是作为mname来标记backstackrecord, 在实际的处理中,它是否为null根本不重要。
当然,我们也可以自己调用fragmentmanager的popbackstack方法进行回退栈的操作,如果我们想要马上执行的话,就要调用popbackstackimmediate方法,实际上,默认调用的就是这个方法。
如果我们在添加fragment的时候,并没有设置任何tag,但是在弹出栈的时候,要求弹出最新的fragment,增加新的fragment。
fragment的栈并不像是activity的栈那么复杂,提供多种启动模式,如果看源码的话,就会发现,实际上它就只有一种:弹出最近的backstackrecord中的所有fragment。
如果我们调用popbackstack的时候,没有指定flag为pop_back_stack_inclusive,源码中的实现虽然是用if-else分成两种判断情况,但实际的处理是差不多的,不过没有指定的话,它会处理比较麻烦,如果可能的话,我们还是指定一下。
回到我们上面的问题,我们该如何做呢?
replace并不会影响到回退栈,如果我们真的要使用replace来替代某个fragment,并且想要实现回退栈,就要addtobackstack,但如果这时我们想要替换某个fragment,回退栈中的记录并不会跟着被替换,也就是说,这时我们选择回退,会退回到我们被替换的fragment,所以我们必须在替换前就弹出这个fragment。
fragmentmanager提供了getbackstackentrycount方法告诉我们回退栈的数量,还有getbackstackentryat方法来获取到对应的backstackrecord,这时我们就能以下的处理来实现弹出:
if(manager.getbackstackentrycount()>0){ int n = manager.getbackstackentrycount(); manager.popbackstack(manager.getbackstackentryat(n-1).getname(), fragmentmanager.pop_back_stack_inclusive); }
然后我们就能使用replace了。
我们必须注意,add,remove和replace影响到的是fragment在界面上的显示,它们跟回退栈一点关系都没有,实际上,如果我们没有调用addtobackstack,甚至根本就不会有回退栈,而且回退栈是在该方法每次调用后,就会添加一个,不论是否重复,它都不会进行任何判断,所以如果一次fragmenttransaction提交多个fragment,但是只是调用一次addtobackstack,虽然界面上有多个fragment,但是回退栈中只有一个记录。
fragment说归到底,在源码上来看,就只是和activity生命周期同步的view,它不可能做到和activity一样复杂的功能,它的任何逻辑业务代码,实际上也属于activity,只不过移动到另一个类中而已,当然,如果愿意的话,就算把它当做一个轻量级的viewcontroller也是可以的,毕竟它只是负责自己负责的view的一切业务功能。
fragmenttransaction为fragment提供了add,remove,hide,show和replace几种操作,我们要注意的是,add和replace的区别。
replace实际上就是remove + add的结合,并且使用replace的话,每次切换的话,会导致fragment重新创建,因为它会把被替换的fragment从视图中移除,这样当替换回来的时候,就要重新创建了。
这样频繁切换,就会严重影响到性能和流量。
所以,官方的说法是:replace()
这个方法只是在上一个fragment不再需要时采用的简便方法。
正确的切换方式是add()
,切换时hide()
,add()
另一个fragment;再次切换时,只需hide()
当前,show()
另一个。
当然,在hide之前,我们还需通过isadd来判断是否添加过。
如果通过hide和show来实现切换,我们就不需要保存数据,因为fragment并没有被销毁,如果是replace这种方式,我们就要保存数据,举个例子,如果界面中有edittext,我们如果想要保存之前在edittext的输入,就要保存这个值,不然使用replace的话,是会移除整个view的。
fragment还涉及到和activity以及其他fragment的通信。
最好的方式就是只让activity和fragment进行通信,如果fragment想要和其他fragment进行通信,也得通过activity。
我们可以利用回调fragment的方法进行通信,当然,也可以在fragment中声明接口,只要activity实现这些接口,就能实现activity和fragment的通信。
想到setarguments是通过bundle的形式来保存数据,那么我们是否可以利用这点,在传参上做一点文章呢?
在软件设计上,为了减少依赖,提议利用一个高层抽象来负责组件之间的通信,这样各个组件之间就不需要互相依赖了,也就是所谓的依赖倒置原则。
那么,我们这里是否也可以利用这个原则来做点事情呢?
依赖倒置在很多框架中的表现是采取注解的形式,我们可以考虑一下注解的方式来解决这个问题。
如果仅仅是为了构建fragment而传输的参数,问题倒是比较简单,只要合理的利用反射,我们就可以获取到fragment的字段,然后赋值。
类似的表现形式如下:
class fragmenta extends fragment{ @arg private int age; public void oncreate(){ fragmentinject.inject(this); } } class activitya extends activity{ public voi oncreate(){ fragmenta a = new fragmenta(); bundle bundle = new bundle(); bundle.putstring("text", "你好"); a.setarguments(bundle); fragmentmanager manager = getsupportfragmentmanager(); fragmenttransaction transaction = manager.begintransaction(); transaction.add(r.id.container, a); transaction.commit(); } }
实际上,这种方式无非就是代码组织方式上的改变,因为我们完全可以在fragment的oncreate中获取到bundle,同样也可以进行相同的操作,并且总的代码量会更少,但如果单纯只是从fragment来看,我们只需要调用fragmentinject.inject方法和声明arg注解,其他的东西根本不用考虑,相关的解析bundle和字段赋值都放在fragmentinject这个抽象中,我们就不用每个fragment都要写同样的代码,只要交给fragmentinject就行。
当然,上面只是简单的实现,真的是要实现一个成熟的东西是要考虑很多方面的,我们这里就把这个简单的项目放在github上:https://github.com/wenjiang/fragmentargs.git,如果有新的想法,欢迎补充。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。