常见问题分析:Fragment在Activity被回收后重叠

简介

现象

我们常在使用fragment时偶现莫名其妙的重叠现象,分明正确的按照顺序调用了add、hide以及show方法
中间两个Fragment重叠了

代码

代码相对比较简单,即通过点击button触发fragment的展示与隐藏

class MainActivity : AppCompatActivity(), View.OnClickListener {

    var homeFragment = HomeFragment()
    var favoriteFragment = FavoriteFragment()
    var currFragment: Fragment? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.i("wzt", "onCreate : " + javaClass.simpleName)
        setContentView(R.layout.activity_main)
        changeFragment(homeFragment)
        button1.setOnClickListener(this)
        button2.setOnClickListener(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.i("wzt", "onDestroy : " + javaClass.simpleName)
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.button1 -> changeFragment(homeFragment)
            R.id.button2 -> changeFragment(favoriteFragment)
        }
    }

    private fun changeFragment(fragment: Fragment) {
        Log.i("wzt", "changeFragment : $fragment")
        if (currFragment == fragment) {
            return
        }
        val transaction = supportFragmentManager.beginTransaction()
        currFragment?.let { transaction.hide(it) }
        if (!fragment.isAdded) {
            transaction.add(R.id.fl_content, fragment)
        }
        transaction.show(fragment)
        currFragment = fragment
        transaction.commitAllowingStateLoss()
    }


}

问题分析

日志分析

如log打印,在横竖屏切换后正常情况下会触发Activity的重建操作

//应用启动,默认显示HomeFragment
07-05 00:33:00.239 3694-3694/com.kyrie.proj.blog I/wzt: onCreate : MainActivity
07-05 00:33:00.275 3694-3694/com.kyrie.proj.blog I/wzt: changeFragment : HomeFragment{928ef43}
07-05 00:33:00.279 3694-3694/com.kyrie.proj.blog I/wzt: onCreateView: HomeFragment{928ef43 #0 id=0x7f07003f}
//点击button切换到FavoriteFragment
07-05 00:33:36.037 3694-3694/com.kyrie.proj.blog I/wzt: changeFragment : FavoriteFragment{18279c6}
07-05 00:33:36.038 3694-3694/com.kyrie.proj.blog I/wzt: onCreateView: FavoriteFragment{18279c6 #1 id=0x7f07003f}
//屏幕旋转,触发Activity销毁重建
//两个Fragment与MainActivity均被销毁
07-05 00:34:07.777 3694-3694/com.kyrie.proj.blog I/wzt: onDestroyView: HomeFragment{928ef43 #0 id=0x7f07003f}
07-05 00:34:07.779 3694-3694/com.kyrie.proj.blog I/wzt: onDestroyView: FavoriteFragment{18279c6 #1 id=0x7f07003f}
07-05 00:34:07.779 3694-3694/com.kyrie.proj.blog I/wzt: onDestroy : MainActivity
//MainActivity开始重建,注意此处会新建两个不同的HomeFragment对象
07-05 00:34:07.790 3694-3694/com.kyrie.proj.blog I/wzt: onCreate : MainActivity
07-05 00:34:07.792 3694-3694/com.kyrie.proj.blog I/wzt: changeFragment : HomeFragment{b07bb2}
07-05 00:34:07.792 3694-3694/com.kyrie.proj.blog I/wzt: onCreateView: HomeFragment{e811703 #0 id=0x7f07003f}
07-05 00:34:07.793 3694-3694/com.kyrie.proj.blog I/wzt: onCreateView: FavoriteFragment{1a6a0fe #1 id=0x7f07003f}
07-05 00:34:07.793 3694-3694/com.kyrie.proj.blog I/wzt: onCreateView: HomeFragment{b07bb2 #2 id=0x7f07003f}

这里稍微说一下Fragment的toString方法打印出的内容

//b07bb2 对象id
//#2 Fragment中的mIndex,在Fragment列表里的位置
//0x7f07003f Fragment中的mFragmentId,Fragment的id,静态创建可以在<fragment>里设置,而我们的动态创建就是所在的容器的id,此处输出的均为容器fl_content的id
HomeFragment{b07bb2 #2 id=0x7f07003f}

从最后旋转屏幕后的代码看出,虽然只调用了一次changeFragment,但是会触发三个Fragment的创建,其中有两个不同的HomeFragment对象。

原因

  1. 从日志很明显的得知,因为重走了MainActivity的onDestroy与onCreate方法,旋转屏幕会导致Activity会被回收,其内部的Fragment也均被回收。
  2. 但是Activity被系统回收时会通过onSaveInstanceState方法保存相关界面数据用于重建,数据中就包含Fragment有关的数据
  3. 重建时onCreate方法中的参数savedInstanceState即为第一步保存的Bundle数据,Activity根据savedInstanceState恢复被系统回收前fragment的状态,因此仍然会显示FavoriteFragment
  4. 在上面的恢复显示了FavoriteFragment的基础上,MainActivity仍然会重新走一遍我们所写的初始化方法调用changeFragment,因此导致又重新展示了HomeFragment

在MainActivity重建后,逻辑如下

//MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
        //super.onCreate中会恢复所保存的Fragment数据,并重新并展示对应的Fragment
        super.onCreate(savedInstanceState)
        Log.i("wzt", "onCreate : " + javaClass.simpleName)
        setContentView(R.layout.activity_main)
        //这里又重新去展示了一个新的homeFragment对象
        changeFragment(homeFragment)
        button1.setOnClickListener(this)
        button2.setOnClickListener(this)
    }

private fun changeFragment(fragment: Fragment) {
        //1.传入了新HomeFragment,且此时currFragment为null
        Log.i("wzt", "changeFragment : $fragment")
        if (currFragment == fragment) {
            return
        }
        val transaction = supportFragmentManager.beginTransaction()
        //因为currFragment为null,所以也不会去隐藏恢复出来的旧Fragment
        currFragment?.let { transaction.hide(it) }
        //接下来在不隐藏原先fragment的基础上重新添加了新Homefragment到界面,导致重叠
        if (!fragment.isAdded) {
            transaction.add(R.id.fl_content, fragment)
        }
        transaction.show(fragment)
        currFragment = fragment
        transaction.commitAllowingStateLoss()
    }

源码分析

Fragment的存储

下面贴一小段源码,来看看MainActivity是如何在销毁时保存Fragment信息的

//Activity.java
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        //...
        //此处会调用到FragmentManagerImpl的saveAllState方法,保存了界面所有Fragment数据到Parcelable数据中
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            //存入outState
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        //...
    }

接下来看看saveAllState如何保存Fragment数据到Parcelable,这里只展示了部分有关代码

//FragmentManagerImpl.java
Parcelable saveAllState() {
    if (mActive.isEmpty()) {
        return null;
    }
    // First collect all active fragments.
    //1.首先收集当前active的Fragment,转化为ArrayList<FragmentState>
    ArrayList<FragmentState> active = new ArrayList<>(size);
    for (Fragment f : mActive.values()) {
        FragmentState fs = new FragmentState(f);
        active.add(fs);
        if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
            //saveFragmentBasicState方法除了会保存fragment自身的一些属性,还会遍历保存fragment的子fragment有关信息
            fs.mSavedFragmentState = saveFragmentBasicState(f);
        }
    }
    
    ArrayList<String> added = null;
    BackStackState[] backStack = null;
    
    // Build list of currently added fragments.
    // 构建一个当前已添加的fragment列表
    added = new ArrayList<>();
    for (Fragment f : mAdded) {
        added.add(f.mWho);
    }
    
    // Now save back stack
    // 保存BackStack信息
    if (mBackStack != null) {
        int size = mBackStack.size();
        backStack = new BackStackState[size];
        for (int i = 0; i < size; i++) {
            backStack[i] = new BackStackState(mBackStack.get(i));
        }
    }
    
    FragmentManagerState fms = new FragmentManagerState();
    fms.mActive = active;
    fms.mAdded = added;
    fms.mBackStack = backStack;
    return fms;
}
  1. 上面我们发现mActive中的所有Fragment对象都被转化为FragmentState
  2. 关于mActive与mAdded的区别在Fragment那点事④mAdded&mActive这篇文章中写的很清楚,有兴趣可以详细了解一下。总结一下就是mActive=mAdded+在返回栈中等待被恢复的Fragment

具体如何从Fragment转化成FragmentState就不再赘述了,我们直接来看FragmentState里面有些什么属性就知道存了些什么内容

//FragmentState.java
final class FragmentState implements Parcelable {
    final String mClassName;//类名
    final String mWho;//fragment内部的唯一名称,通过UUID.randomUUID()直接生成的随机值
    final boolean mFromLayout;//是否直接从layout文件中生成的
    final int mFragmentId;//fragment的id,是我们日志中打印出的id
    final int mContainerId;//容器id
    final String mTag;//注意这里Fragment的tag也被存储了!!!
    final boolean mRetainInstance;//是否在Activity重创建时候保留Fragment实例
    final boolean mRemoving;//状态标识
    final boolean mDetached;//状态标识
    final Bundle mArguments;//fragment构建时的参数
    final boolean mHidden; //状态标识
    final int mMaxLifecycleState;//看注释是最大可以到达的状态,默认是RESUME
    
    Bundle mSavedFragmentState;//一些其他属性,包括子Fragment的属性

    //这个fragment对象不会序列化,是根据上面的数据所新生成的,所以可以忽略它
    Fragment mInstance;
}

总结:在Activity的onSaveInstanceState方法中,会存储Fragment的tag等数据,在重建时取出

Fragment的恢复

Fragment的重建会从Activity的onCreate开始

扫描二维码关注公众号,回复: 11396324 查看本文章
//Activity.java
protected void onCreate(@Nullable Bundle savedInstanceState) {
    //...
    if (savedInstanceState != null) {
        //从savedInstanceState取出onSaveInstanceState保存的fragment数据
        Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
        mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.fragments : null);
    }
    mFragments.dispatchCreate();
}
//FragmentManagerImpl.java
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
    //...
    restoreSaveState(state);
}

void restoreSaveState(Parcelable state) {
    FragmentManagerState fms = (FragmentManagerState)state;
    // Build the full list of active fragments, instantiating them from
    // their saved state.
    mActive.clear();
    //遍历mActive列表
    for (FragmentState fs : fms.mActive) {
        //通过FragmentState生成新Fragment对象
        Fragment f = fs.instantiate(mHost.getContext().getClassLoader(),
                        getFragmentFactory());
        f.mFragmentManager = this;
        mActive.put(f.mWho, f);
    }
    
    mAdded.clear();
    //根据mAdded里存储的fragment的标识key,从mActive中取出对应Fragment并标识为added状态
    for (String who : fms.mAdded) {
        Fragment f = mActive.get(who);
        f.mAdded = true;
        mAdded.add(f);
    }
    
    //下面的mBackStack就先省略了,免得被绕晕
}

由上面代码得知,根据FragmentState的instantiate方法生成新的对象,具体就不跟下去了,看到入参可以大概知道是根据classLoader新生成Fragment对象,再把存储的数据进行赋值给得到的新对象

源码部分总结

  1. onSaveInstanceState保存了当前Activity的所有Fragment数据
  2. onCreate中对保存的数据进行了恢复,且状态与之前一致

解决方法

  1. 问题原因总结就是fragment被恢复,但是activity重建后仍然重走了我们所写的fragment的初始化流程,我们所需要的是不再重新走fragment的初始化流程,而是用以前的fragment就好
  2. 前面的源码分析可以得知,被恢复的fragment仍然保留原有设置的tab,那么我们可以在重新new Fragment对象之前通过tag来确认是否已经被还原了对应的fragment,如果已经有了相同tag的Fragment直接取出就好了
    //入参为tag,每个fragment改为通过tag区分
    private fun changeFragment(tag: String) {
        val fragment = initFragment(tag)
        Log.i("wzt", "changeFragment : $fragment")
        if (currFragment == fragment) {
            return
        }
        val transaction = supportFragmentManager.beginTransaction()
        currFragment?.let { transaction.hide(it) }
        if (!fragment.isAdded) {
            //这里给fragment指定了tag
            transaction.add(R.id.fl_content, fragment, tag)
        }
        transaction.show(fragment)
        currFragment = fragment
        transaction.commitAllowingStateLoss()
    }

    //根据tag初始化Fragment
    private fun initFragment(tag: String): Fragment {
        //注意这里:先从FragmentManager里通过tag获取fragment实例
        var fragment = supportFragmentManager.findFragmentByTag(tag)
        //获取不到才重新初始化
        if (fragment == null) {
            fragment = when (tag) {
                TAG_FAVORITE -> FavoriteFragment()
                TAG_HOME -> HomeFragment()
                else -> HomeFragment()
            }
        }
        return fragment
    }
  1. 在上面的代码实现之后,就不会出现重建后同时有两个HomeFragment对象的问题了。
  2. 但是上面这样还不够,虽然fragment还原后状态不变,但是currFragment重建后为null了,并且我们在onCreate里固定调用了changeFragment(TAG_HOME),这会导致若重建之前显示的是FavoriteFragment时仍然会出现重叠现象,因此我们还需要在重建之前记录当前显示的是哪个Fragment
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        //记录当前Fragment的tag
        currFragment?.let{
            outState.putString(KEY_TAG, it.tag)
        }
    }

并在onCreate时取出,不再直接到onCreate中直接再次展示HomeFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.i("wzt", "onCreate : " + javaClass.simpleName)
        setContentView(R.layout.activity_main)
        //从savedInstanceState中取到保存的tag,默认为TAG_HOME
        val lastTag = savedInstanceState?.getString(KEY_TAG, TAG_HOME) ?: TAG_HOME
        changeFragment(lastTag)
        button1.setOnClickListener(this)
        button2.setOnClickListener(this)
    }

总结

至此就算从根本上解决了Activity被系统回收之后多个Fragment重叠的异常现象,正常效果就不放了,最后总结一下解决方法

  1. 为Activity的每个Fragment指定一个tag,并优先根据tag从FragmentManager拉取,若没拉取到才去初始化Fragment
  2. 在onSaveInstanceState保存当前展示的fragment的tag,重建后从onCreate取出,根据此tag决定默认展示的Fragment

参考

  1. Fragment不为人知的细节
  2. fragment重叠的完美解决方案

猜你喜欢

转载自blog.csdn.net/wangzici/article/details/107148749