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

总结

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

(0)

相关推荐

  • Android Fragment多层嵌套重影问题的解决方法

    1解决bug的思想: //step1:当bug被发现(排除极低偶然性,单次性,开发工具导致) //step2:根据经验判断bug的重现场景,多次测试,直到精准的定位bug //step3:根据重现场景找到对应的代码 //step4:分析区域代码是否会影响到其他功能. //step5:做好数据的备份工作.(做好代码重构和恢复的准备,这样你才能肆无忌惮的捣鼓代码) //step6:修复代码的过程中,你会发现可能有多种解决方案.试着采取不影响主线的解决方案.以免影响到其他的代码. //step7:回顾

  • Android实现Tab布局的4种方式(Fragment+TabPageIndicator+ViewPager)

    Android现在实现Tab类型的界面方式越来越多,今天就把常见的实现方式给大家来个总结.目前写了: 1.传统的ViewPager实现 2.FragmentManager+Fragment实现 3.ViewPager+FragmentPagerAdapter实现 4.TabPageIndicator+ViewPager+FragmentPagerAdapter 1.传统的ViewPager实现 主要就是ViewPager+ViewAdapter这个还是比较常见的,就不多说了 效果图: 代码: p

  • Android开发技巧之Fragment的懒加载

    前言 所谓懒加载,就是当fragment完全可见的时候我们再去加载数据,我们在做应用开发的时候,一个Activity里面可能会以viewpager(或其他容器)与多个Fragment来组合使用,而如果每个fragment都需要去加载数据,或从本地加载,或从网络加载,那么在这个activity刚创建的时候就变成需要初始化大量资源.这样的结果,我们当然不会满意.那么,能不能做到当切换到这个fragment的时候,它才去初始化呢? 答案就在Fragment里的setUserVisibleHint这个方

  • Android用Fragment创建选项卡

    本文结合之前的动态创建fragment来进行一个实践,来实现用Fragment创建一个选项卡 项目布局 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout

  • Android Activity与Fragment实现底部导航器

    单Activity多Fragment实现底部导航器 最近由于Android基础知识讲解需要,采用单Activity多Fragment实现类似QQ底部导航器示例,这种开发模式广泛应用于App开发,比如QQ,微信,新浪等,关于Android底部导航栏的实现方式特别多,实现也是五花八门,同时Google在自己推出的Material design中也增加了Bottom Navigation导航控制,实现起来更加简单,且支持动态效果更加酷炫,但是因为是基础的知识,所以打算通过自定义来实现,不使用Botto

  • Android Fragment+FragmentTabHost组件实现常见主页面(仿微信新浪)

    采取的方法是Fragment+FragmentTabHost组件来实现这种常见的app主页面的效果 首先给出main.xml文件 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"

  • Android Fragment与Activity之间的相互通信实例代码

    Android Fragment与Activity之间的相互通信 前言 自从Android3.0引入Fragment之后,主要是为了支持动态灵活的界面设计和屏幕的适配问题.Fragmenty不能单独存在,必须依赖Activity作为视图展示的一部分,同事它具有自己的生命周期,接收它自己的事件,具有更加灵活的特性,如今Fragment已经被广泛的应用到App开发中,最常见的就是单Activity多Fragment的模式.Fragment依赖于Activity而存在,就不可避免需要与Activity

  • Android中关于FragmentA嵌套FragmentB的问题

    问题描述: 在项目中Activity A中嵌套Fragment B,Fragment B中再嵌套Fragment C,如图: 问题1:在点击Activity A中主菜单1进行切换时,报错Fragment C already added. 解决:在Framgent B中添加Fragment C 调用add()时先判断fragmentC.isAdded() FragmentManager fm=getActivity().getSupportFragmentManager(); FragmentTr

  • Android Fragment(动态,静态)碎片详解及总结

    Android Fragment(动态,静态)碎片详解 一.Fragment的相关概念(一)Fragment的基础知识 Fragment是Android3.0新增的概念,中文意思是碎片,它与Activity十分相似,用来在一个 Activity中描述一些行为或一部分用户界面.使用多个Fragment可以在一个单独的Activity中建 立多个UI面板,也可以在多个Activity中使用Fragment. Fragment拥有自己的生命 周期和接收.处理用户的事件,这样就不必在Activity写一

  • Android Fragment的生命周期详解

    Fragments的生命周期 每一个fragments 都有自己的一套生命周期回调方法和处理自己的用户输入事件. 对应生命周期可参考下图: 详解Android Fragment之二:Fragment的创建和生命周期 创建片元(Creating a Fragment) To create a fragment, you must create a subclass of Fragment (or an existing subclass of it). The Fragment class has

  • Android 中 Fragment 嵌套 Fragment使用存在的bug附完美解决方案

    自从Android3.0引入了Fragment之后,使用Activity去嵌套一些Fragment的做法也变得更加流行,这确实是Fragment带来的一些优点,比如说:Fragment可以使你能够将activity分离成多个可重用的组件,每个都有它自己的生命周期和UI,更重要的是Fragment解决了Activity间的切换不流畅,实现了一种轻量及的切换,但是在官方提供的android.support.v4包中,Fragment还是或多或少的存在一些BUG,今天就与大家分享一下这些BUG和解决方

随机推荐