• Android Fragment 要你何用?2.0版本


    作者:小鱼人爱编程

    1. 老生常谈:为什么需要Fragment?

    先看Activity、Fragment、View三者的关系:

    Activity 拥有生命周期,但是需要和AMS通信(跨进程),比较臃肿。
    View 不需要和AMS通信,但没有生命周期,不好处理复杂的逻辑(如网络请求数据渲染到View上)。
    而Fragment介于两者之间,它拥有生命周期(借助于Activity),无需与AMS通信,速度快。

    Fragment更多时候充当着中介的作用:

    将Activity里复杂的UI逻辑分离出来放到Fragment里,将对View复杂的操作分离到Fragment里。

    一言蔽之,使用Fragment的理由:

    1. 无需跨进程,轻量级
    2. 拥有生命周期

    2. Fragment 的生命周期与重建流程

    与Activity生命周期对比

    Fragment创建分为动态和静态,以动态创建Fragment为例分析:

    这么看Fragment生命周期的回调方法比Activity更多,还好大部分是成对出现有迹可循。
    通常情况下,我们只需要重写onCreateView(),提供自定义界面就好。

    Fragment 重建流程

    现象

    先看简单Demo:在Activity的onCreate里添加Fragment:

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.layout_fragment )
            val fragment = FishPureFragment()
            fragment.arguments = Bundle().apply {
                putString("hello", "fragment:${count++}")
            }
            supportFragmentManager.beginTransaction().add(R.id.root, fragment).commitNow()
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Activity 显示的同时Fragment也显示了,一切看起来很正常,此时我们将手机横竖屏切换一下:

    可以看出,随着横竖屏的切换,创建的Fragment越来越多。
    此种重建现象在很多场景下并不符合需求,比如进入Activity后拉取接口并显示弹窗,若使用DialogFragment展示弹窗,则Activity重建后会出现多个弹窗。

    原理

    我们知道,Fragment的生命周期依赖于Activity,当横竖屏切换时候Activity进行了重建,同时会查看关联该Activity的所有Fragment是否需要重建,若是则进行重建。
    第一次显示Fragment后,横竖屏切换导致Activity重建,此时也会重建Fragment,而在Activity.onCreate()里又新建了Fragment,因此此时Activity里关联了2个Fragment。

    核心点在于红色部分:

    1. Activity 销毁时保存Fragment状态
    2. Activity 重建时根据状态恢复并展示Fragment

    除了横竖屏会导致Activity重建,其它配置项变更、系统内存紧张kill掉Activity等也会引起Activity重建

    Fragment重建引发的Crash

    和Activity等四大组件不一样的是:我们可以直接创建Fragment实例。
    若是我们重写了Fragment默认构造函数:

    class FishPureFragment(val str:String):Fragment() 
    
    • 1

    在Fragment重建的时候会Crash:

        java.lang.RuntimeException: Unable to start activity ComponentInfo{com.fish.kotlindemo/com.fish.kotlindemo.fragment.dialogFragment.FishFragmentActivity}: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment com.fish.kotlindemo.fragment.dialogFragment.FishPureFragment: could not find Fragment constructor
    
    • 1

    原因是Fragment重建时会去寻找默认构造函数构造新的实例,而我们重写了构造函数,此时已经不存在默认的无参构造函数,当然会Crash。
    因此,通常来说无需重写Fragment构造函数,若是想要传递参数给Fragment,可使用Fragment.setArguments()。

    如何禁止Fragment重建

    有以下几种方式:
    第一种:
    既然是Activity重建引发的Fragment重建,那么釜底抽薪,禁止Activity重建。
    比如禁止屏幕旋转时重建Activity,可以在AndroidManifest.xml里配置:

    android:configChanges="orientation|screenSize"
    
    • 1

    第二种:
    不想改配置,也可以从Activity保存的状态入手,Frament重建依赖于恢复状态:

        //FragmentActivity.java
        private void init() {
            addOnContextAvailableListener(new OnContextAvailableListener() {
                @Override
                public void onContextAvailable(@NonNull Context context) {
                    //Activity.onCreate里会调用此方法
                    mFragments.attachHost(null /*parent*/);
                    //找到需要恢复的Fragment状态
                    Bundle savedInstanceState = getSavedStateRegistry()
                            .consumeRestoredStateForKey(FRAGMENTS_TAG);
    
                    if (savedInstanceState != null) {
                        //不为空则进行Fragment恢复,重建
                        Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
                        mFragments.restoreSaveState(p);
                    }
                }
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    也就是说只要恢复状态为空,那么Fragment就不会进行重建,而该状态是从Activity.onCreate(Bundle)里传递过来的,因此只需要在Activity里进行如下设置:

        override fun onCreate(savedInstanceState: Bundle?) {
    //        super.onCreate(savedInstanceState)
            //状态置空
            super.onCreate(null)
            setContentView(R.layout.layout_fragment )
            val fragment = FishPureFragment()
            fragment.arguments = Bundle().apply {
                putString("hello", "fragment:${count++}")
            }
            supportFragmentManager.beginTransaction().add(R.id.root, fragment).commitNow()
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    第三种:
    将Activity下的所有恢复状态置空有点粗暴了,会影响到其它组件如View的恢复,而我们仅仅只需要禁止Fragment重建,使用如下方式:

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            savedInstanceState?.let {
                it.getBundle("androidx.lifecycle.BundleSavedStateRegistry.key")?.remove("android:support:fragments");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注:此种方式与系统实现版本有关,有些版本取不到value

    3. Fragment add/remove/replace/hide/show的快速理解

    add

    添加一个Fragment实例到Fragment管理栈里,此间Fragment生命周期经历了从onAttach()逐渐到onActivityCreated()。
    此时Fragment所绑定的布局(View)已经添加到指定的父布局里了。

    当管理栈里已有一个Fragment实例:Fragment1,此时再往里面添加Fragment2,Fragment2入栈并将其绑定的布局添加到父布局里。
    依据父布局属性不同,有不同的展示方式:

    1. 若父布局是FrameLayout,则Fragment2绑定的布局将会覆盖Fragment1绑定的布局。
    2. 若父布局是LinearLayout,则Fragment2绑定的布局将会添加到Fragment1绑定的布局左边/右边。

    remove

    将一个Fragment实例从Fragment管理栈里移除,此间Fragment生命周期经历了从onDestroyView()逐渐到onDetach()。
    此时Fragment所绑定的布局(View)已经从指定的父布局里移除了。

    replace

    当Fragment管理栈里没有Fragment实例时,replace与add效果一致。
    当Fragment管理栈里有Fragment1实例时,replace(Fragment2)将触发Fragment1的生命周期从onDestroyView()逐渐到onDetach(),而Fragment2生命周期从onAttach()逐渐到onActivityCreated(),最终展示Fragment2。
    replace 可简单理解为remove+add。

    hide

    不会触发Fragment生命周期,仅仅只是把Fragment绑定的布局隐藏(GONE)。

    show

    hide的反向操作,不会触发Fragment生命周期,仅仅只是把Fragment绑定的布局显示(VISIBLE)。

    4. Fragment与ViewModel的结合

    Activity ViewModel与FragmentManagerViewModel

    如上图,Activity ViewModelStore里存储着不同的ViewModel实例,其中FragmentManagerViewModel是与Fragment有关的。
    当Activity销毁重建时,在Activity销毁阶段ViewModelStore并没有被释放,而是被保留下来,等到Activity重建时继续使用,而ViewModelStore实例不变,其内部的ViewModel也不变,这就是ViewModel的原理。

    那么FragmentManagerViewModel 是什么时候添加到ViewModelStore里的呢?
    入口在这:

        #FragmentActivity.java
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            //绑定FragmentManager
            mFragments.attachHost(null /*parent*/);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最终调用到:

        #FragmentManager.java
        void attachController(@NonNull FragmentHostCallback host,
                              @NonNull FragmentContainer container, @Nullable final Fragment parent) {
            //...
            if (parent != null) {
                mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
            } else if (host instanceof ViewModelStoreOwner) {
                //host 即为承载的Activity
                //取出Activity ViewModelStore
                ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
                mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
            } else {
                mNonConfig = new FragmentManagerViewModel(false);
            }
        }
    
        #FragmentManagerViewModel.java
        static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
            ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
                    FACTORY);
            //从Activity的ViewModelStore 取出FragmentManagerViewModel
            //若没有,则创建FragmentManagerViewModel实例
            return viewModelProvider.get(FragmentManagerViewModel.class);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    答案是:Activity.onCreate()将FragmentManagerViewModel添加到Activity ViewModelStore里。

    FragmentManagerViewModel与Fragment的ViewModel

    现在FragmentManagerViewModel可以在Activity重建时恢复,那么它和Fragment里的ViewModel又是如何关联的呢?

    FragmentManagerViewModel 里有个成员变量:

    private final HashMap mViewModelStores = new HashMap<>();
    
    • 1

    在Fragment里声明ViewModel:

    private val vm by viewModels()
    
    • 1

    查看viewModels调用链,关键要找到对应的ViewModelStore,而Fragment的ViewModelStore获取方式如下:

    #Fragment.java
        public ViewModelStore getViewModelStore() {
            if (mFragmentManager == null) {
                throw new IllegalStateException("Can't access ViewModels from detached fragment");
            }
            return mFragmentManager.getViewModelStore(this);
        }
    
        #FragmentManagerViewModel.java
        ViewModelStore getViewModelStore(@NonNull Fragment f) {
            //mViewModelStores 是Map,key 是Fragment唯一标识符 value 为Fragment对应的ViewModelStore
            ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
            if (viewModelStore == null) {
                //不存在则创建
                viewModelStore = new ViewModelStore();
                //放入map
                mViewModelStores.put(f.mWho, viewModelStore);
            }
            return viewModelStore;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    到此,流程就比较清晰了:

    FragmentManagerViewModel里的mViewModelStores里存储着所有Fragment的ViewModelStore。也即是Fragment的ViewModel实际上是存储在FragmentManagerViewModel里的。

    接着来整体捋一下Fragment的ViewModel是如何在重建时保持的。

    显而易见,Fragment ViewModel实际上是间接依赖Activity ViewModeStore。

    Fragment 关联的lifecycleScope

    lifecycleScope 监听着Fragment生命周期,若是Fragment被销毁,则lifecycleScope也会被取消。

    Fragment ViewModel关联的viewModelScope

    viewModelScope 与ViewModel 生命周期保持一致,若是ViewModel被销毁(Fragment被销毁而非重建),则viewModelScope也会被取消。

    5. DialogFragment(Dialog和Fragment的结晶)

    DialogFragment 使用

    普通的Dialog并没有生命周期,而不关联生命周期的Dialog处理异步请求比较麻烦,此时DialogFragment出现了。

    class MyDialogFragment : DialogFragment() { 
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val tv = TextView(context).apply {
                text = "hello world"
                setTextColor(Color.RED)
                textSize = 30f
            }
            return tv
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    显示DialogFragment:

    MyDialogFragment().show(supportFragmentManager, "dd")
    
    • 1

    我们只需要定义Fragment所绑定的布局,最终布局将会显示在Dialog里。

    DialogFragment 原理

    Dialog 显示Fragment绑定的布局

    你也许比较好奇:之前添加的Fragment都是指定父布局,将Fragment所绑定的布局添加到父布局里,那此时的DialogFragment所指定的父布局在哪呢?
    从show方法入手:

        public void show(@NonNull FragmentManager manager, @Nullable String tag) {
            //就是添加普通的Fragment流程
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        }
    
        #FragmentTransaction.java
        public FragmentTransaction add(@NonNull Fragment fragment, @Nullable String tag)  {
            //没有指定父布局
            doAddOp(0, fragment, tag, OP_ADD);
            return this;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    与添加普通Fragment不同的是此时并没有指定Fragment的父布局。

    Dialog会监听Fragment生命周期:

        #DialogFragment.java
        private Observer mObserver = new Observer() {
            public void onChanged(LifecycleOwner lifecycleOwner) {
                if (lifecycleOwner != null && mShowsDialog) {
                    //拿到Fragment绑定的View
                    View view = requireView();
                    if (mDialog != null) {
                        //将View添加到Dialog里
                        mDialog.setContentView(view);
                    }
                }
            }
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    当生命周期回调后拿到Fragment绑定的View添加到Dialog里。
    如此一来,Fragment就完成了和Dialog的配合显示界面。

    Dialog的创建时机

        public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
            return super.onCreateDialog(savedInstanceState);
        }
    
    • 1
    • 2
    • 3

    重写该方法可以自定义Dialog或是监听默认Dialog的创建。
    值得注意的是:在onAttach()/onCreate()里是拿不到Dialog对象的,因为那时候还没有创建Dialog,它在onCreateView()之前创建的。

    6. ViewPager2(Fragment与RecyclerView的结晶)

    ViewPager2使用

    除了DialogFragment,Fragment也与RecyclerView配合,形成了ViewPager2。
    创建Adapter:

    public class VPAdapter extends FragmentStateAdapter {
    
        private List list = new ArrayList<>();
    
        public VPAdapter(@NonNull FragmentActivity fragmentActivity, List list) {
            super(fragmentActivity);
            this.list = list;
        }
    
        @NonNull
        @Override
        public Fragment createFragment(int position) {
            return list.get(position);
        }
    
        @Override
        public int getItemCount() {
            return list.size();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    ViewPager2绑定Adapter:

            VPAdapter vpAdapter = new VPAdapter(this, list);
            viewPager2.setAdapter(vpAdapter);
    
    • 1
    • 2

    可以看出,使用起来很简洁,ViewPager2的显示依靠着Fragment。

    ViewPager2原理

    RecyclerView缓存

    在此之前先简单介绍一下RecyclerView(简称RV)的缓存设计。
    RV缓存的是什么呢?
    缓存的是ViewHolder(简称VH),VH里持有待显示的子布局(View)。

    RecyclerView里有个内部类:Recycler,里面有5个缓存变量,归为3种缓存,分别为:
    一级缓存(默认2个+1个预拉取)、二级缓存、三级缓存(默认5个,区分itemType)。
    当RecyclerView渲染布局显示item时,先分别从一、二、三级缓存寻找可用的VH,若没有找到则重新创建子布局及其所属的VH,最终渲染。

    VH有两种状态:

    1. VH保持着当前的数据状态,此种状态下当VH复用时可直接使用,对应一级缓存。
    2. VH仅仅保持着View,没有绑定数据,此种状态下当VH复用时需要重新绑定数据,也就是走onBindViewHolder()方法,对应三级缓存。

    二级缓存是暴露给调用者设置自定义缓存的,此处先忽略。
    先看看一、三级缓存是如何填充数据的。

    再看RV是如何从缓存取数据的:

    RecyclerView与Fragment联动

    核心代码在FragmentStateAdapter.java里。
    RecyclerView关键动作与Fragment的联动:

    本质上还是将Fragment的View添加到RV提前构造的ItemView内。

    我们来梳理一下ViewPager2(简称VP2)的滑动场景。

    1. VP2展示第一个元素,实际展示的是RV的第一个元素,此时RV回调了onCreateViewHolder、onBindViewHolder等回调,Fragment也走了生命周期,最终Fragment绑定的View添加到了RV的子Item里
    2. 当滑动VP2到第二个元素(下标为1)时,RV的第一个元素被放到一级缓存,此时RV触发移除子Item,注意这个时候并没有销毁Fragment
    3. 当滑动VP2回到第一个元素时,RV仅仅只需要addView即可,不会触发Fragment生命周期
    4. 当滑动VP2到后面几个元素时,此时一级缓存已满,将放到三级缓存里,然后触发子Item的回收,这个时候会移除对应的Fragment,Frament走生命周期里的销毁流程
    5. RV的预取元素时,会走onCreateViewHolder、onBindViewHolder回调,但不会触发addView,也就是不会触发Fragment的生命周期

    VP2的缓存

    public void setOffscreenPageLimit(@OffscreenPageLimit int limit)
    
    • 1

    设置VP2左右缓存的数量,默认是没有缓存的,也不能将缓存设置为0,只能是>=1。

    VP2的缓存和RV的缓存是什么关系呢?
    假设VP2缓存limit=1,再来梳理一下ViewPager2(简称VP2)的滑动场景。

    1. VP2展示第一个元素,实际展示的是RV的第一个元素,此时RV回调了onCreateViewHolder、onBindViewHolder等回调,Fragment也走了生命周期,最终Fragment绑定的View添加到了RV的子Item里。与此同时,VP2继续渲染第二个元素,与第一个元素步骤一致
    2. 当VP2滑动到第二个元素时,由于之前已经渲染过,此时是直接展示(RV无需addView)。于此同时会继续提前缓存第三个元素。
    3. 当VP2滑动到第一个元素时,,由于之前已经渲染过,此时是直接展示。

    可以看出VP2的缓存实际上是提前将RV的元素渲染了,若设置了limit=1,那么此时RV活跃的Item有三个:当前1个+左右各一个。
    当设置了VP2的缓存后,意味着多缓存了Fragment实例。

    以上是Fragment实际应用与原理的相关内容。

    Android 学习笔录

    Android 性能优化篇:https://qr18.cn/FVlo89
    Android Framework底层原理篇:https://qr18.cn/AQpN4J
    Android 车载篇:https://qr18.cn/F05ZCM
    Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
    Android 音视频篇:https://qr18.cn/Ei3VPD
    Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
    OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
    Kotlin 篇:https://qr18.cn/CdjtAF
    Gradle 篇:https://qr18.cn/DzrmMB
    Flutter 篇:https://qr18.cn/DIvKma
    Android 八大知识体:https://qr18.cn/CyxarU
    Android 核心笔记:https://qr21.cn/CaZQLo
    Android 往年面试题锦:https://qr18.cn/CKV8OZ
    2023年最新Android 面试题集:https://qr18.cn/CgxrRy
    Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
    音视频面试题锦:https://qr18.cn/AcV6Ap

  • 相关阅读:
    Mysql表关联简单介绍(inner join、left join、right join、full join不支持、笛卡尔积)
    pytest框架中的pytest.ini配置文件
    git命令学习
    自己实现 SpringMVC 底层机制 系列之-实现任务阶段 8- 完成返回 JSON 格式数据-@ResponseBody
    保洁企业怎么实施智能软件增加客户的互动
    结构性设计模式之装饰器模式
    ubuntu安装idea
    使用正则匹配获取url中的host
    java.util.Optional
    《位图BitMap - 基于java实现》
  • 原文地址:https://blog.csdn.net/maniuT/article/details/134470539