ViewPager中Fragment状态保存的哪些事

简介: 在使用 ViewPager 时 , 如果我们的适配器使用的是 FragmentStatePagerAdapter ,那么当我们重新滑到之前已销毁的页面时,一般情况下页面的状态依然将保持不变(比如 RecyclerView 的 滚动位置等,EditText 的 输入内容 等), 或者说 View 历史状态被还原了。本文的主旨就是解释其 保存与还原内部的原理以及过程。

基础概念

ViewPager 官方的适配器有两种,即 FragmentPagerAdapter 以及 FragmentStatePagerAdapter 。前者适用于少量Item时,后者适用于多个item。

主要原因是 FragmentStatePagerAdapter 每次会重建以及 销毁Fragment, 而 FragmentPageAdapter 并不会销毁实例,只是对视图做了 attachdetach

举个 🌰

如下段代码所示,我们有这样一个适配器 [MainAdapter]:

class MainAdapter(fragmentManager: FragmentManager, private val datas: List<String>) :
    FragmentStatePagerAdapter(fragmentManager) {
    override fun getCount(): Int {
        return datas.size
    }
    override fun getItem(position: Int): Fragment {
        return T1Fragment.newInstance(datas[position])
    }
}

其余代码比较简易,我们用以下层级即可代表:

MainActivity 
 ViewPager(adapter = MainAdapter , offscreenPageLimit = 1)
    Fragment(key) - (by activityViewModel)
        RecyclerView - (data = activityViewModel.data[key])

如上所示,我们有一个 Activity,其内部有一个 ViewPager,ViewPager 的适配器就是我们上面写的 MainAdapter,默认缓存 n(1)+2 。

Fragment 内部是一个 RecyclerView,其数据源来自 activity级 的ViewModel(即我们对数据根据key做了缓存,避免每次的重新初始化)

我们做一个滚动测试,然后再看看 Fragment 重新创建后 View状态(RecyclerView滚动位置) 的变化,如下所示:

网络异常,图片无法展示
|

因为默认缓存为 n(1)+2 ,即当我们滑动到 item=3 时,1 页面此时已被销毁。但当我们重新切换到 1 时,可以发现,Fragment1 中 RecyclerView滚动位置 没有变化,所以可以证明 Fragment 的状态的确是被还原了。

那这是怎么做的呢? 带着这个问题,我们开始比较简单的源码解析环节。

Adapter解析

直接去看 FragmentStateAdapter

  ...
  // 保存Fragment的状态list
  private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
  ...

其内部有一个名为 mSavedState 的List,用于保存我们的 Fragment状态 ,那这个 mSavedState 又会在哪里被调用呢?既然要还原以及保存,那就免不了两个地方,[初始化] 与 [销毁] ,所以我们继续往下去看 instantiateItem() 与 destroyItem()。

destroyItem()

此方法用于销毁我们的指定Fragment,其内部把当前Fragment的状态根据下标保存到了 mSavedState 中。

public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment) object;
    ...
    // 避免数组长度不足导致的越界异常
    while (mSavedState.size() <= position) {
        mSavedState.add(null);
    }
    // 调用 mFragmentManager 去保存Fragment 的状态,并将其保存在了内部的 mSavedState 中
    mSavedState.set(position, fragment.isAdded()
            ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
    // 销毁fragment
    mFragments.set(position, null);
    ...
}

instantiateItem()

此方法主要用于初始化 指定position 对应的 Fragment 。

在初始化 Fragment 时,其会通过 下标position 从 mSavedState 找到缓存

存的 Fragment 状态,然后将设置给其,便于后续的使用。

public Object instantiateItem(@NonNull ViewGroup container, int position) {
    // 如果fragment已存在直接返回
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
    // 初始化Fragment,在adapter中,我们需要重写此方法,实现我们的Fragment初始化
    Fragment fragment = getItem(position);
    if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
    // 数组健壮性保护
    if (mSavedState.size() > position) {
        // 获取指定位置保存的状态
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            // 将状态重新设置给fragment
            fragment.setInitialSavedState(fss);
        }
    }
    ...
    return fragment;
}

小结

所以我们可以简单理解为 FragmentStatePagerAdapter 之所以可以做到状态还原,是因为其在销毁 Fragment 时,默认缓存了当前 Fragment 的状态信息,并且以下标的方式进行了保存,当我们在滑动 ViewPager 时,其会加载并初始化指定 position 所对应 Fragment ,并将缓存的 Fragment 的状态信息 set 进去。

Fragment部分

通过上面的方式,我们可以简单的知道 ViewPager 是如何帮我们进行状态还原与保存,那 Fragment 到底是在什么时候去使用这个状态呢?所以带着这个问题,我们接着去看看 Fragment 的源码。

无论是 View 还是 Fragment ,其都具有 这个方法 onSaveInstanceState ,既然有保存的方法,那肯定也有还原的方法。

在Fragment中我们去看这个方法:onViewStateRestored()

官方解释,此方法被调用时意味着 Fragment所有状态 都已经还原。

所以我们直接去看看到底是在哪里调用了此方法,也就知道 Fragment 是怎么还原状态的。具体的调用栈如下:

  1. FragmentManager - moveToState() 👇🏻
  2. FragmentManager - activityCreated() 👇🏻
  3. Fragment - performActivityCreated() 👇🏻
  4. Fragment - restoreViewState() 👇🏻
  5. Fragment - restoreViewState(Bundle) 👇🏻

FragmentManager

// 1.
// fragment状态变化
void moveToState(){
   switch (f.mState) {
       // 当view已经创建好时
       case Fragment.VIEW_CREATED:  fragmentStateManager.activityCreated();
   }
}
// 2. 通知活动已创建
void activityCreated() {
    // 执行fragment的 ActivityCreated 方法,相当于fragment与act已绑定
        mFragment.performActivityCreated(mFragment.mSavedFragmentState);
    // 调度Fragment的生命周期
        mDispatcher.dispatchOnFragmentActivityCreated(
                mFragment, mFragment.mSavedFragmentState, false);
 }

Fragment

// 3. 执行与act绑定时的逻辑
void performActivityCreated(Bundle savedInstanceState) {
        mChildFragmentManager.noteStateNotSaved();
        mState = AWAITING_EXIT_EFFECTS;
        mCalled = false;
        // 触发
        onActivityCreated(savedInstanceState);
        ...
        restoreViewState();
        mChildFragmentManager.dispatchActivityCreated();
}
// 4. 恢复视图状态
private void restoreViewState() {
        if (mView != null) {
            restoreViewState(mSavedFragmentState);
        }
        mSavedFragmentState = null;
}
// 恢复具体的视图状态
final void restoreViewState(Bundle savedInstanceState) {
    // 视图状态不为null,则恢复之前的视图层级
    if (mSavedViewState != null) {
        mView.restoreHierarchyState(mSavedViewState);
        mSavedViewState = null;
    }
    if (mView != null) {
        mViewLifecycleOwner.performRestore(mSavedViewRegistryState);
        mSavedViewRegistryState = null;
    }
    mCalled = false;
    // 通知view的状态已被还原
    onViewStateRestored(savedInstanceState);
    ..
}

总结

  • 当我们使用 ViewPager 时,如果使用 FragmentStatePagerAdapter 作为适配器,Fragment 的状态会被主动还原,主要原因是:

  Fragment 销毁时,会调用 destoryItem 方法,adapter内部会主动保存了当前的                      Fragment 状态,并以当前下标作为 key 存到了一个list集合中,然后在调用 getItem()  初始化Fragment时,其会将之前保存的状态重新 set 给我们的 Fragment 实例。

  • 当 Fragment 生命周期执行到 activityCreated 时,从而调用 restoreViewState() 触发View状态的恢复(此时onCreateView已执行),然后将我们的view状态还原上去。

知道了这个概念,我们也就可以自己做一些小扩展,比如我们可以在部分情况下主动将我们的Fragment状态保存起来,以便在后面进行恢复,也即就是使用以下两个方法即可。

// 保存
FragmentManager.saveFragmentInstanceState(fragment)
// 还原
Fragment.setInitialSavedState(SavedState)
目录
相关文章
RecyclerView禁止复用
RecyclerView禁止复用
2091 0
|
消息中间件 druid Kafka
从Apache Flink到Kafka再到Druid的实时数据传输,用于分析/决策
从Apache Flink到Kafka再到Druid的实时数据传输,用于分析/决策
272 0
|
Kubernetes 容器
Kubernetes(K8S) 镜像拉取策略 imagePullPolicy
Kubernetes(K8S) 镜像拉取策略 imagePullPolicy
270 0
|
XML Java 程序员
Java一分钟之-AOP:面向切面编程
【6月更文挑战第13天】Java中的AOP允许程序员定义切面,将日志、事务等通用功能与业务逻辑解耦。切面包括通知(Advice,如前置、后置等)和切入点(Pointcut,定义执行点)。Spring框架通过代理和@AspectJ注解支持AOP。常见问题包括代理对象理解错误、切入点表达式错误、环绕通知处理不当和配置遗漏。理解和实践中,AOP能提升代码可维护性和可扩展性。
464 5
|
XML API 数据格式
Fragment 这些 API 已废弃,你还在使用吗?
Fragment 这些 API 已废弃,你还在使用吗?
344 1
|
Windows
DiskGenius硬盘分区及数据恢复软件
DiskGenius是一款硬盘分区及数据恢复软件。它是在最初的DOS版的基础上开发而成的。Windows版本的DiskGenius软件,除了继承并增强了DOS版的大部分功能外(少部分没有实现的功能将会陆续加入),还增加了许多新的功能。如:已删除文件恢复、分区复制、分区备份、硬盘复制等功能。
696 1
|
Android开发 数据格式 XML
【我的Android进阶之旅】如何隐藏Android中EditText控件的默认下划线
Android EditText控件是经常使用的控件,但是有时候我们并不需要它的一些默认的属性,比如说下划线,因为有时候这样的默认下划线看起来特别怪异,和其他控件在一起搭配的时候不协调,因此有时候就需要去掉默认的下划线。
2182 0
|
编解码 Linux
基于Asterisk的VoIP开发指南——(1)实现基本呼叫功能
原文: 基于Asterisk的VoIP开发指南——(1)实现基本呼叫功能 说明:        1.本文档探讨基于Asterisk如何实现VoIP的一些基本功能,包括基本呼叫功能的方案选取、主叫号码透传、如何编写Asterisk AGI程序、Radius认证计费模块等。
3959 0
|
机器学习/深度学习 编解码 人工智能
ECCV2022 | 多模态融合检测新范式!基于概率集成实现多模态目标检测
我相信大家不多不少都会看过我自己做的一些工作,同时也还有我解读RGB-Thermal系列的一些工作,所以这一期我想讨论一下RGB-T目标检测的工作!
ECCV2022 | 多模态融合检测新范式!基于概率集成实现多模态目标检测
|
消息中间件 存储 Java
Kotlin中正确的使用Handler
如果`Handler`在`Activity`中是以非静态内部类的方式初始化的,那么`Handler`默认就会持有`Activity`的实例,因为在`Java`中:**非静态内部类默认会持有外部类的实例,而静态内部类不会持有外部类的实例**
675 0