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,如果有新的想法,欢迎补充。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。

声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#cainiaojc.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。