简介
现象
我们常在使用fragment时偶现莫名其妙的重叠现象,分明正确的按照顺序调用了add、hide以及show方法
代码
代码相对比较简单,即通过点击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对象。
原因
- 从日志很明显的得知,因为重走了MainActivity的onDestroy与onCreate方法,旋转屏幕会导致Activity会被回收,其内部的Fragment也均被回收。
- 但是Activity被系统回收时会通过onSaveInstanceState方法保存相关界面数据用于重建,数据中就包含Fragment有关的数据
- 重建时onCreate方法中的参数savedInstanceState即为第一步保存的Bundle数据,Activity根据savedInstanceState恢复被系统回收前fragment的状态,因此仍然会显示FavoriteFragment
- 在上面的恢复显示了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;
}
- 上面我们发现mActive中的所有Fragment对象都被转化为FragmentState
- 关于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开始
//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对象,再把存储的数据进行赋值给得到的新对象
源码部分总结
- onSaveInstanceState保存了当前Activity的所有Fragment数据
- onCreate中对保存的数据进行了恢复,且状态与之前一致
解决方法
- 问题原因总结就是fragment被恢复,但是activity重建后仍然重走了我们所写的fragment的初始化流程,我们所需要的是不再重新走fragment的初始化流程,而是用以前的fragment就好
- 前面的源码分析可以得知,被恢复的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
}
- 在上面的代码实现之后,就不会出现重建后同时有两个HomeFragment对象的问题了。
- 但是上面这样还不够,虽然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重叠的异常现象,正常效果就不放了,最后总结一下解决方法
- 为Activity的每个Fragment指定一个tag,并优先根据tag从FragmentManager拉取,若没拉取到才去初始化Fragment
- 在onSaveInstanceState保存当前展示的fragment的tag,重建后从onCreate取出,根据此tag决定默认展示的Fragment