美文网首页AndroidAndroidAndroid开发
洞若观火:Fragment不为人知的细节

洞若观火:Fragment不为人知的细节

作者: 三雒 | 来源:发表于2018-05-06 21:42 被阅读415次

    前言

    Fragment在日常开发中非常常用,但是你有没有想过它到底是怎样的一种存在呢?

    其实可以简单地认为它就是一个“控件”,更加具体一点就是“View控制器”。它把自己承载的View展示到容器View中,自身有生命周期,并能添加逻辑控制视图。

    其主要具有以下优点:

    • 复用, 一个Fragment可以多个Activity使用
    • 解耦, 能将业务逻辑模块分离
    • 灵活, 能够比较容易适配手机和平板,根据屏幕的宽度决定Fragment的放置
    • 支持回退栈等操作

    虽然Fragment有它的优点,但是在实际的使用的过程中有很多要理解和注意的细节。本文主要是带大家讨论一下这些细节,主要从两个重点切入,一个是Fragment的生命周期,另一个是Activity重创建引发的问题。阅读本文需要对Fragment以及Activity重创建有一定了解。

    Activity重创建是指由于系统配置改变(屏幕方向,字体大小等)或者内存不足 引起对Activity的销毁与重建

    注:本文基于support 26.1.0

    support.v4 还是android.app

    到目前为止所有的设备都是api 15以上了,那是不是应该用android.app.Fragment?当然不是
    这个其实google的文档里有说明过,大致有以下两点原因:

    1)Fragment的发展过程中有bug,support.v4的Fragment可以及时解决一些bug

    2)Fragment的发展过程中增加新特性,support.v4的Fragment可以兼容所有版本。比如Android P增加的LifeCycle特性。
    总之就是不断更新,可以兼容所有版本,当然你的Activity需要使用FragmentActivity的子类配合使用。

    关注Activity重创建对Fragment的影响

    在Activity的onCreate创建Fragment要先判断是否已经包含

    有些初学者在Activity onCreate方法中动态创建Fragment时候会这么写:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_life_cycle);
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            Fragment fragment = new FragmentOne();
            transaction.add(R.id.container, fragment,FRAGMENT_ONE_TAG);
            transaction.commit();
        }
    

    这种写法是有问题的,因为Activity在重建的时候FragmentManager会根据之前保存的Fragment相关的状态帮我们重新创建Fragment实例,因此上面的写法会导致每次多创建一个Fragmnet。因此我们要先检查一下FragmentManager是否有该Fragment

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_life_cycle);
            fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_ONE_TAG);
            if (fragment == null) {
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                fragment = new FragmentOne();
                transaction.add(R.id.container, fragment,FRAGMENT_ONE_TAG);
                transaction.commit();
            }
        }
    

    这里不知道你是否会困惑FragmentManager究竟为我们保存了Fragment的哪些状态数据?这些要被保存的信息保存在FragmentState类中(至于为什么是这个类,不是本文的重点),我们来看一下这个类的字段:

    final class FragmentState implements Parcelable {
        final String mClassName;
        final int mIndex;
        final boolean mFromLayout;
        final int mFragmentId;
        final int mContainerId;
        final String mTag;
        final boolean mRetainInstance;
        final boolean mDetached;
        final Bundle mArguments;
        final boolean mHidden;
        
        ...
    }
    
    • mClassName类名,重新创建Fragment实例时候用到的
    • mIndex,在Fragment列表里的位置
    • mFragmentId,Fragment的id,静态创建可以在<fragment>里设置,动态创建就是所在的容器的id
    • mContainerId,Fragment所在的容器的id
    • mTag 标签
    • mRetainInstance,是否在Activity重创建时候保留Fragment实例
    • mDetached ,是否对该Fragment执行了detach操作
    • mArguments,setArguments的参数
    • mHidden,是hide还是show

    使用setArguments传递参数,而不是构造函数

    不知道你有没有想过,为什么我们在创建Frament实例时候,官方推荐我们使用setArguments来传递参数,而不是构造函数。其实看上面FragmentState保存了mArguments,就应该恍然大悟,使用setArguments在Activity重创建时候也会帮我我们保存与恢复参数值。

    注意保存与恢复我们自己的业务数据

    没错,Activity重创建时候FragmentManager已经帮我们保存和恢复了Fragment的实例和部分状态,但是我们业务数据的保存和恢复还是需要靠我们自己来动手。与Activity类似,我们在Fragment的onSaveInstanceState保存数据,然后再在onCreate,onCreateView或者onActivityCreated方法中都可以进行恢复。

    虽然国内很多应用都是固定竖屏的,但是你还要考虑应用在后台内存不足的情况以及其他配置变化引起得Activity重建,虽然可能发生次数较少,但是嘛严谨的程序员还是尽可能处理这些情况

    生命周期

    image

    这是一张Fragment的生命周期图,没错,就是这么复杂,这也是刚使用Fragment感觉问题很多的原因。只单纯地去搞懂每个生命周期方法的意义和作用不利于我们对整体的把握,下面我们从另外两个方面更全面地理解这些生命周期的作用和意义。

    与Activity生命周期的关系

    在这种情况下Fragment就像是一个牵线木偶,它的所有生命周期都是由Actvity驱动,这一点你只要看一下FramentActivity生命周期方法就很容易明白。

    在Activty的onCreate方法中创建Fragment,生命周期的交叉关系如下:

    LifeCycleActivity: onCreate 
        FragmentOne: onAttach
        FragmentOne: onCreate 
        FragmentOne: onCreateView
        FragmentOne: onActivityCreated
    

    在Actvity的onCreate方法执行完后,分别执行了Fragment以上的四个生命周期,其实onActivityCreated 的调用是发生在FragmentActivity的onStart方法中。另外在Activity重创建时候,Fragment的onAttach和onCreate方法会先于Activity的onCreate方法执行。
    之后分别执行了onStart和onResume方法,可能和想象的顺序有所差别,不过也没什么影响,不同版本的库表现可能会有所差异。

     FragmentOne: onStart
    LifeCycleActivity: onStart
    
    LifeCycleActivity: onResume
        FragmentOne: onResume
    

    按下Back键后:

        FragmentOne: onPause
    LifeCycleActivity: onPause
    
        FragmentOne: onStop
    LifeCycleActivity: onStop
    
        FragmentOne: onDestroyView
        FragmentOne: onDestroy
        FragmentOne: onDetach
    LifeCycleActivity: onDestroy
    

    分别执行了Fragment和Activiyt的onPause,onStop方法。然后Fragment销毁试图(onDestroyView),销毁实例(onDestroy),取消与Activity的关联(onDetach),之后执行Activity的onDestroy。
    总体上这种交叉关系还是比较对称的,就像Android中很多生命周期方法调用顺序一样。

    与Fragment管理方法之间的关系

    理解调用某个管理Fragment的方法之后,Fragment到底执行了哪些生命周期方法,主要是能更好区别这些方法之间的区别。

    • add

      向containerViewId中添加一个Fragment实例,此时Fragment会执行

      onAttach-> onCreate-> onCreateView-> onActivityCreated-> onStart ->onResume

    • remove

      与add操作相反,移除一个Fragment实例,如果移除时候这个fragment没有被添加到回退栈里,就会销毁这个Fragment的实例。执行下面的生命周期:
      onPause-> onStop-> onDestroyView-> onDestroy-> onDetach
      如果移除时把这个事物加入回退栈,就好比下面的代码:

      FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
      transaction.remove(fragment);
      transaction.addToBackStack(null);
      transaction.commit();
      

      Fragment实例就不会被销毁,只执行如下的生命周期方法:

      onPause->onStop->onDestroyView

      另外多说一点,添加回退栈是指将当前事务添加回退栈,当按下Back键时候会做一个和该事务相反的操作。

    • replace

      先移除所有containerViewId中的实例,然后添加一个新的Fragment实例。

    • detach

      销毁Fragment的视图,但保留实例。

      onPause->onStop->onDestroyView

    • attach

      与detach相对,重新创建Fragment视图。
      onCreateView->onActivityCreated->onStart->onResume

    • hide

      隐藏Fragment,只是设为不可见状态,并不执行任何生命周期方法,不销毁实例,也不销毁视图,就好像只是把Fragmnet的视图设置为INVISIBLE。此时Fragment的mHidden为true,会引起onHiddenChanged()回调调用

    • show

      与hide对应,显示出Fragment,同样不执行任何生命周期方法。

    1)需要注意的是视图销毁之后,界面上保留的一些数据也就没有啦。比如用户在EditText中输入了数据,你执行remove并把事务添加到回退栈,按下back键,这时候EditText中就没有数据啦。如果你希望保留数据就用hide。

    2)Fragment实例销毁,视图自然销毁。但视图销毁,实例不一定销毁,不用多解释了吧。

    3)一般情况下Fragment实例销毁才会执行onDetach方法,但是还有另外一种情况3就是Fragment实例还在,Activity实例却销毁了,也很好理解,你关联的Actvity实例都没有啦,这种情况其实就是我们下面要说的setRetainInstance.

    setRetainInstance

    Fragment还有另外一个非常神奇的设计,就是调用了setRetainInstance(true)的Fragment在Activity重创建时候不会销毁Fragment的实例,只是会销毁视图并detach,不会执行onDestroy。在Activity销毁重建期间执行如下的生命周期方法:
    onPause-> onStop ->onDestroyView ->onDetach
    onAttach -> onCreateView-> onActivityCreated-> onStart->onResume
    但是这种Fragment有什么用呢?在Activity重创建期间,一些简单的数据我们可能通过onSaveInstanceState方法保存就很方便啦,但是有时候会遇见很复杂的数据,此时我们利用Fragment保留实例的这种特性,创建一个没有界面的Fragment用来保存数据是非常方便的一种做法。比如我们在实现MVVM架构的App时候,ViewModel通常就比较复杂,我会创建如下的Fragment类保存ViewModel。

    public class ViewModelHolder<VM> extends Fragment {
    
        private VM mViewModel;
    
        public ViewModelHolder() { }
    
        public static <M> ViewModelHolder createContainer(@NonNull M viewModel) {
            ViewModelHolder<M> viewModelContainer = new ViewModelHolder<>();
            viewModelContainer.setViewModel(viewModel);
            return viewModelContainer;
        }
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);
        }
    
        @Nullable public VM getViewModel() {
            return mViewModel;
        }
    
        public void setViewModel(@NonNull VM viewModel) {
            mViewModel = viewModel;
        }
    }
    

    然后通过以下方法去查找FragmentManger中是否有该ViewModelHolder,如果有的话就取到之前保存的数据啦。

     public static <T> T findViewModel(@NonNull FragmentManager fragmentManager, String viewModelTag) {
      
            ViewModelHolder<T> retainedViewModel =
                    (ViewModelHolder<T>) fragmentManager
                            .findFragmentByTag(viewModelTag);
    
            if (retainedViewModel != null && retainedViewModel.getViewModel() != null) {
                return retainedViewModel.getViewModel();
            }
            return null;
        }
    

    总之这个特性还是很强大的。

    重叠?不存在的

    只要你理解并处理好上面所说的问题,同时使用24.0.0以上版本的support库的话,一般就不会再出现Fragment的重叠问题啦。

    24.0.0以下版本bug:

    24.0.0之前的support库有一个bug,就是在FragmentManager保存Fragment实例状态的时候,没有保存mHidden,因此重创建之后Fragment都处于显示状态就造成了重叠。以下是截图:

    image
    所以呢,最好使用24.0.0以上版本的support库。假如你必须要使用24.0.0以下版本的话,那就自己在Fragment的onSaveInstanceState保存isHidden(),然后再onCreate去显示或隐藏。
    public class BaseFragment extends Fragment {
        private static final String IS_HIDDEN = "IS_HIDDEN";
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
             if (savedInstanceState != null) {
                boolean isHidden = savedInstanceState.getBoolean(IS_HIDDEN);
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                if (isHidden) {
                    ft.hide(this);
                } else {
                    ft.show(this);
                }
                ft.commit();
            }
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            outState.putBoolean(IS_HIDDEN, isHidden());
        }
    }
    

    注意Fragment是否创建了多个实例

    在Activity onCreate中创建实例的话,一定要注意不要重复add。

    总之倘若发生Fragment重叠,要搞清楚容器View里此时有哪几个Fragment实例,每个实例是显示还是隐藏,问题就能够得到解决。

    getActivity()

    这个我本来不想说,但网上经常有人遇到getActivity()为null的问题。这往往都是因为在onAttach之前或者onDetach之后调用了getActivity,
    其实在Fragment在onAttach之后,onDetach之前getActivity都是不会为null的。
    因此开发者要处理好自己的逻辑代码,避免在不正确的时机调用getActivity。比如Fragment有一个网络请求发送出去,
    这时候候Fragment已经销毁了,并执行了onDetach,然后网络请求回调调用了getActivity。这其实就是内存泄漏了,应该
    在Fragment onDestroy的时候就打断网络请求回调引用链或者使用弱引用。
    而有些同学会在onAttach方法中自己再保存一个Activity的引用,这种做法在我看来是错误的。

    DialogFragment

    DialogFragment是继承自Fragment,使用它来创建对话框明显的好处就是Activity重创建时候对话框能像Fragment一样被恢复。如果你对它不是很熟悉的话,推荐鸿神的
    Android 官方推荐 : DialogFragment 创建对话框

    当Fragment遇上ViewPager

    Fragment和ViewPager组合使用,也有非常多有意思的细节。限于篇幅原因,请查阅洞若观火:当Fragment遇上ViewPager

    本文到这里就结束啦,个人水平和精力有限,如有错误打脸轻点。

    相关文章

      网友评论

      • 键盘上的麒麟臂:DialogFragment切换到桌面再进入,或者切换到其它应用再回来时,会有个重新弹出的效果,这个问题怎么解决。比如说你给Dialog加了动画,比如从下往上弹出,然后你切到桌面,再切回应用,它会再执行一次从下往上弹出的动画。
      • Magic丶海:博主好,你文章中提到一句话“而有些同学会在onAttach方法中自己再保存一个Activity的引用,这种错发在我看来是错误的。” 我对这句话不太理解,为什么是错误的呢?
        我目前的做法是,在BaseFragment声明一个全局变量Activity,然后在
        onAttach(Context context)方法里,将context强转为activity并赋值。而且有时候在fragment某些地方调用getActivity(),as会出现警告,说getActivity()可能会为null
        冰冰的冻结:是不是只能在使用的时候判断 getActivity 是否为空呢,否则没有别的方法解决这个问题吧
        Magic丶海:@三雒 感谢解答,我受益颇多
        三雒:hi,我是这样认为的,调用getActivity为空的情况要么是在onAttach之前,要么是在onDeatch之后。列举几种情况吧:
        1.onAttach之前为空。你在Activity的onCreate方法中直接或间接调用到fragmet.getActivity,这时候onAttach可能还没执行,保存引用的做法也救不了你啊。
        2.onDeatch之后为空。这要分为两种情况。第一种一个普通的Fragment而言,执行了onDetach意味着这个Fragment实例已经销毁了,那么你就应该在onDestroy时候终止所有操作,移除所有回调,总之把收尾工作做好;第二种是setRetainInstance的Fragment,因为它执行onDetach时候有可能并没有销毁,比如屏幕旋转。但是这种情况下,是你的Activity销毁了,你调用也必然为空。我理解的可能并不全面,欢迎交流。

      本文标题:洞若观火:Fragment不为人知的细节

      本文链接:https://www.haomeiwen.com/subject/qwkprftx.html