近期在修改页面结构过程中,遇到了一个特殊的问题,现在做个总结。
一、背景介绍
先说下Feed首页的页面结构,为了满足运营多种动态化的需求,Feed首页采用了以下图所示的页面结构,从页面到最小粒度的控件可以分成5个层级
- HomePageActivity
- 负责解析跳转参数、页面埋点、初始化等相关逻辑
- TabContainerFragment
- Homepage真正的界面主体是通过该Fragment去承载的,内部是可以切换的一个tablayout;
- 满足了运营对“HP上的tabs可以动态配置,灵活添加”
- TabItemFragment
- 由FeedBaseFragment延伸出来的各种不同Feed流, 主体是一个列表,呈现Feed流信息;
- 满足了运营对“不同的tab可以动态配置数据源,以呈现不同的内容聚合性”
- FeedItem
- 具体的一个Feed信息;
- 满足了运营对“不同的Feed类型要有不同的Feed样式进行承载、甚至是氛围区别”;
- Pdp
- Feed信息所包含的商品图片、或者自定义图片
- 满足了运营对“Feed可以透出商品图或者自定义图等信息,来吸引用户的眼球,快速表述feed的语义”;
再介绍下修改的原由:应用原来的使用的主框架是tabhost + activity,最近android架构升级要求替换为FragmentTabHost,为了同时满足并行开发和尽量保证代码逻辑改变不大, 因此计划将原FeedHomePageActivity中的业务再次封装为一个FeedHomeFragment给框架团队,其内部逻辑和FeedHomePageActivity保持一致。
其中一些细节调整不再赘述,我们进入本篇的正题: 在将Activity替换为Fragment后,我们发现原来Viepager中的TabFragment所设计的基于setMenuVisibility的可视生命周期监听失效了,而该生命周期承载了我们重要的业务功能,由此引发了我们对原因的分析。
二、问题描述
TabItemFragment 原来使用了 setMenuVisibility 来判断页面是否对用户可见
private boolean isExitViewpager = false;
protected boolean isViewCreated = false;
protected boolean isMenuVisible = false;
@Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
LLog.i(TAG, String.format("%s : %s : %s : %s", pageTagMark(), "setUserVisibleHint", menuVisible, isViewCreated));
isMenuVisible = menuVisible;
if (!menuVisible
&& isExitViewpager
&& isViewCreated) {
onPagePause();
}
if (menuVisible
&& isExitViewpager
&& isViewCreated) {
onPageStart();
}
}
@Override
public void onResume() {
super.onResume();
LLog.i(TAG, String.format("%s : %s : %s : %s", pageTagMark(), "onResume", isViewCreated, isMenuVisible));
if (isExitViewpager) {
if (isViewCreated && isMenuVisible) {
onPageStart();
}
} else {
onPageStart();
}
}
@Override
public void onPause() {
super.onPause();
LLog.i(TAG, String.format("%s : %s", pageTagMark(), "onPause"));
if (isExitViewpager) {
if (isMenuVisible) {
onPagePause();
}
} else {
onPagePause();
}
}
当顶级容器为Activity时该判断表现良好,但是当在新框架中FragmentTabHost中时,发现再次回到Feed频道后,TabItemFragment#onResume方法中的isMenuVisible竟然变为了false, 这就引发了onPageStart()方法没有调用!
通过初步分析,我们发现新老结构的差异在于,新框架下重回Feed渠道时会触发FragmentStatePagerAdapter.restoreState,该函数调用了setMenuVisibility(false),哪怕页面时处于显示状态的
那么这个函数是什么作用,为什么会被新框架调用呢? 我们从新框架引发的变化开始分析
三、FragmentTabHost 对FeedHomeFragment生命周期的影响
我们知道Fragment的常见生命周期如下:
- onAttach
- onCreate
- onCreateView
- onViewCreated
- onResume
- onPause
- onStop
- onDestroyView
- onDestroy
- onDetach
我们对fragment进行管理时,往往使用的是FragmentManager, 其中有两对关键函数:
- add\remove(replace)
- remove从Activity中移除一个Fragment时,如果被移除的Fragment没有添加到回退栈,这个Fragment实例将会被销毁
- 被移除的Fragment的调用顺序为:onPause, onStop, onDestroyView, onDestroy, onDetach
- 被移除的Fragment再次加入后顺序为:从onAttach开始
- 如果remove后,使用addToBackStack函数
- 只存在三种状态的切换:onPause,onStop,onDestroyView
- 仅仅只是界面被销毁onDestroyView,而fragOne对象的实例依然被保存在FragmentManager中(因为无onDestroy,onDetach),它的部分状态依然被保存在FragmentManager中
- 再次加入会直接从onCreateView开始
- attach() detach()
- 重建view视图,附加到UI上并显示
- attach()->onCreateView()->onActivityCreated()->onStart()->onResume()
- detach()->onPause()->onStop()->onDestroyView()
- show()/hide()
- 使用show()/hide()时一般是会使用addToBackStack,,因为要使用show()/hide()前提是该Fragment实例已经存在,只不过我们是否将其界面显示出来
- hide从Activity中隐藏一个Fragment时,不会触发上述的生命周期函数
那么 FragmentTabHost 使用的是哪种方式呢?我们进入源码分析,入口从 onTabChanged(String tabId) 开始:
public void onTabChanged(String tabId) {
if (this.mAttached) {
FragmentTransaction ft = this.doTabChanged(tabId, (FragmentTransaction)null);
if (ft != null) {
ft.commit();
}
}
if (this.mOnTabChangeListener != null) {
this.mOnTabChangeListener.onTabChanged(tabId);
}
}
其中会调用doTabChanged(@Nullable String tag, @Nullable FragmentTransaction ft)去获取一个FragmentTransaction,并commit提交
@Nullable
private FragmentTransaction doTabChanged(@Nullable String tag, @Nullable FragmentTransaction ft) {
FragmentTabHost.TabInfo newTab = this.getTabInfoForTag(tag);
if (this.mLastTab != newTab) {
if (ft == null) {
ft = this.mFragmentManager.beginTransaction();
}
if (this.mLastTab != null && this.mLastTab.fragment != null) {
ft.detach(this.mLastTab.fragment);
}
if (newTab != null) {
if (newTab.fragment == null) {
newTab.fragment = Fragment.instantiate(this.mContext, newTab.clss.getName(), newTab.args);
ft.add(this.mContainerId, newTab.fragment, newTab.tag);
} else {
ft.attach(newTab.fragment);
}
}
this.mLastTab = newTab;
}
return ft;
}
我们看到其中:
- 对当前的Fragment,调用了FragmentTransaction.detach
- 对tab对应的fragment进行了实例化,如果已经有实例的fragment,会触发FragmentTransaction的attach预操作
我们吧目光转向FragmentTransaction的实现类BackStackRecord:
public FragmentTransaction attach(Fragment fragment) {
this.addOp(new BackStackRecord.Op(7, fragment));
return this;
}
public FragmentTransaction detach(Fragment fragment) {
this.addOp(new BackStackRecord.Op(6, fragment));
return this;
}
内部维护一个操作栈队列( ArrayList<BackStackRecord.Op> mOps = new ArrayList();),执行了对应的入队操纵。 真正的执行是在commit函数中
public int commit() {
return this.commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (this.mCommitted) {
throw new IllegalStateException("commit already called");
} else {
if (FragmentManagerImpl.DEBUG) {
Log.v("FragmentManager", "Commit: " + this);
LogWriter logw = new LogWriter("FragmentManager");
PrintWriter pw = new PrintWriter(logw);
this.dump(" ", (FileDescriptor)null, pw, (String[])null);
pw.close();
}
this.mCommitted = true;
if (this.mAddToBackStack) {
this.mIndex = this.mManager.allocBackStackIndex(this);
} else {
this.mIndex = -1;
}
this.mManager.enqueueAction(this, allowStateLoss);
return this.mIndex;
}
}
我们现在把目光转移到回调BackStackRecord#executeOps():
void executeOps() {
int numOps = this.mOps.size();
for(int opNum = 0; opNum < numOps; ++opNum) {
BackStackRecord.Op op = (BackStackRecord.Op)this.mOps.get(opNum);
Fragment f = op.fragment;
if (f != null) {
f.setNextTransition(this.mTransition, this.mTransitionStyle);
}
switch(op.cmd) {
case 1:
f.setNextAnim(op.enterAnim);
this.mManager.addFragment(f, false);
break;
case 2:
default:
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
case 3:
f.setNextAnim(op.exitAnim);
this.mManager.removeFragment(f);
break;
case 4:
f.setNextAnim(op.exitAnim);
this.mManager.hideFragment(f);
break;
case 5:
f.setNextAnim(op.enterAnim);
this.mManager.showFragment(f);
break;
case 6:
f.setNextAnim(op.exitAnim);
this.mManager.detachFragment(f);
break;
case 7:
f.setNextAnim(op.enterAnim);
this.mManager.attachFragment(f);
break;
case 8:
this.mManager.setPrimaryNavigationFragment(f);
break;
case 9:
this.mManager.setPrimaryNavigationFragment((Fragment)null);
}
if (!this.mReorderingAllowed && op.cmd != 1 && f != null) {
this.mManager.moveFragmentToExpectedState(f);
}
}
if (!this.mReorderingAllowed) {
this.mManager.moveToState(this.mManager.mCurState, true);
}
}
可以看到最终调用了 FragmentManagerImpl的attach|detach, 由此我们也确认了:
- ** FragmentTabHost使用了detach|attach 对fragment进行了操作**
- 切换走时的生命周期调用顺序为:onPause, onStop, onDestroyView
- 再次切换回来时生命周期顺序为从onCreateView、onViewCreated、onResume
- FragmentTabHost简单用法,以及Fragment生命周期
这里解释了,重新回到FeedHomeFragment时它的生命周期是正常被调用的。
由于FeedHomeFragment直接包裹了 FeedTabContainerFragment,那FeedTabContainerFragment 的生命周期也是正常的
那么为什么ViewPage里的TabItemFragment会出现 setMenuVisibility 异常呢?
四、ViewPager遇上Fragment
ViewPager有一个方法setOffscreenPageLimit(int limit),该方法设置保存当前页面两侧各limit个页面,已经超出limit的部分会被销毁,该值默认为1。
假设我们有0,1,2,3四个页面,一开始ViewPager的currentItem为0,此时会预加载1页面;滑动到1页面时候,因为0已经加载过了,此时会预加载2页面;当滑动到2时候,就会销毁0,预加载3。其中加载时会调用PagerAdapter的instantiateItem(ViewGroup container, int position)方法,销毁时会调用destroyItem(ViewGroup container, int position, Object object)方法。
我们看下日志记录:
- 进入第一个页面
- 进入第二个页面
- 当滑动到第二个fragment的时候,可以看到fragment3开始创建
我们可以看到当Fragment遇到ViewPager时,通过生命周期直接判断 界面是否对用户可见已经不可靠了,业界常用的进行真正判断的函数是借助setMenuVisibility 或者 setUserVisibleHint 进行实现的
@Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
if (menuVisible) {
//相当于Fragment的onResume
} else {
//相当于Fragment的onPause
}
}
我们在原框架结构中是使用这种方式去实现的,经过验证也是真实可靠的,但是为什么在新架构中就失效了呢?
进一步的,我们联想到了可能是Fragment的状态保存和恢复引发的异常
五、Fragment的状态保存和恢复
先上大招,看下整体的时序图分析:
-
我们知道FragmentTabHost最终是通过FragmentManager的attachFragment|detachFragment来管理的Fragment的(会触发onattach和ondeatch,会触发viewcreate和destory),就是说ShopStreeMainTabFragment的视图会经历销毁和重建。
-
内部会走到 void moveFragmentToExpectedState(final Fragment f) 对fragment进行操作,其中有几个关键生命周期阶段:
static final int INVALID_STATE = -1; // Invalid state used as a null value.
static final int INITIALIZING = 0; // Not yet created.
static final int CREATED = 1; // Created.
static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.
static final int STOPPED = 3; // Fully created, not started.
static final int STARTED = 4; // Created and started, not resumed.
static final int RESUMED = 5; // Created started and resumed.
- 最终触发moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
- fragment的状态在向前move的时候,即变大到resume状态的过程中
- 向后移动,即隐藏
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
if (f.mState <= newState) {
switch (f.mState) {
...
case Fragment.CREATED:
container = mContainer.onFindViewById(f.mContainerId);
f.mContainer = container;
f.mView = f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
if (container != null) {
container.addView(f.mView);
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
false);
}
f.performActivityCreated(f.mSavedFragmentState);
dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
f.mSavedFragmentState = null;
}
}else{
switch (f.mState) {
...
case Fragment.STOPPED:
case Fragment.ACTIVITY_CREATED:
saveFragmentViewState(f);
f.performDestroyView();
dispatchOnFragmentViewDestroyed(f, false);
}
}
-
那么“进入feed,切到其他tab,再重新切回feed时”,重建过程会依次触发 ShopStreeMainTabFragment的生命周期(ShopStreeMainTabFragment当前状态为1)
- performCreateView
- onViewCreated
- performActivityCreated
- dispatchOnFragmentActivityCreated
- if (f.mView != null) { f.restoreViewState(f.mSavedFragmentState);}
- 重建的fragment会触发 f.restoreViewState(f.mSavedFragmentState); 这个state会有值
-
start
-
resume
4.1 Fragment#restoreViewState(f.mSavedFragmentState)的参数
我们先来看restoreViewState中的源码:
- mInnerView.restoreHierarchyState(this.mSavedViewState); 并回调onViewStateRestored(savedInstanceState);
final void restoreViewState(Bundle savedInstanceState) {
if (mSavedViewState != null) {
mView.restoreHierarchyState(mSavedViewState);
mSavedViewState = null;
}
mCalled = false;
onViewStateRestored(savedInstanceState);
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onViewStateRestored()");
}
}
- 回到 FragmentManager, 传入的mSavedViewState其实是在 隐藏fragment过程中的saveFragmentViewState(fragment)中保存
void saveFragmentViewState(Fragment f) {
if (f.mView == null) {
return;
}
if (mStateArray == null) {
mStateArray = new SparseArray<Parcelable>();
} else {
mStateArray.clear();
}
f.mView.saveHierarchyState(mStateArray);
if (mStateArray.size() > 0) {
f.mSavedViewState = mStateArray;
mStateArray = null;
}
}
- 对于View的saveHierarchyState
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState();
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
// Log.i("View", "Freezing #" + Integer.toHexString(mID)
// + ": " + state);
container.put(mID, state);
}
}
}
- 对于ViewGroup,会遍历子view#onSaveInstanceState();
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.onSaveInstanceState(container);
}
}
}
- 那我们来看下ViewPager的onSaveInstanceState
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
ViewPager.SavedState ss = new ViewPager.SavedState(superState);
ss.position = this.mCurItem;
if (this.mAdapter != null) {
ss.adapterState = this.mAdapter.saveState();
}
return ss;
}
- 调用了adapter的saveState(),对于FragmentStatePagerAdapter就是读取之前fragment.setInitialSavedState(fss);的数组fss, 写入bundle
4.2 遍历嵌套Fragment中
- ShopStreeMainTabFragment会内部调用
- onActivityCreated
- mChildFragmentManager.dispatchActivityCreated)
void performActivityCreated(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = ACTIVITY_CREATED;
mCalled = false;
onActivityCreated(savedInstanceState);
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onActivityCreated()");
}
if (mChildFragmentManager != null) {
mChildFragmentManager.dispatchActivityCreated();
}
}
- 其中dispatchActivityCreated会触发dispatchStateChange
public void dispatchActivityCreated() {
mStateSaved = false;
dispatchMoveToState(Fragment.ACTIVITY_CREATED);
}
private void dispatchMoveToState(int state) {
if (mAllowOldReentrantBehavior) {
moveToState(state, false);
} else {
try {
mExecutingActions = true;
moveToState(state, false);
} finally {
mExecutingActions = false;
}
}
execPendingActions();
}
void moveToState(int newState, boolean always) {
for (int i = 0; i < numAdded; i++) {
Fragment f = mAdded.get(i);
moveFragmentToExpectedState(f);
if (f.mLoaderManager != null) {
loadersRunning |= f.mLoaderManager.hasRunningLoaders();
}
}
}
- moveToState(nextState, false);依次调用持有的child fragments的对应方法生命周期【feedcontainer\following\explore】(当前状态为1))
- 触发feedTabContainerFragment#moveFragmentToExpectedState
- …
- (f.mView != null) { f.restoreViewState(f.mSavedFragmentState);}
- innerView.restoreHierarchyState(this.mSavedViewState);
- FrameLayout# dispatchRestoreInstanceState(container); 遍历子view
- View#dispatchRestoreInstanceState(container);
- ViewPager#onRestoreInstanceState
- mAdapter.restoreState(ss.adapterState, ss.loader);
- FragmentStatePagerAdapter
- View#dispatchRestoreInstanceState(container);
- FrameLayout# dispatchRestoreInstanceState(container); 遍历子view
- innerView.restoreHierarchyState(this.mSavedViewState);
- …
- 触发following#moveFragmentToExpectedState
- …
- 触发explore#moveFragmentToExpectedState
- …
- 触发feedTabContainerFragment#moveFragmentToExpectedState
4.3 FragmentStatePagerAdapter
我们使用了FragmentStatePagerAdapter,它缓存的Fragment是放在mFragments集合中的,当调用destroyItem时候会调用 mFragments.set(position, null)移除对应的实例
@Override
public Object instantiateItem(ViewGroup container, int position) {
//mFragments中保存ViewPager缓存的页面对应的Fragment实例,如果在缓存中就直接返回啦
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
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.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
//保存该Fragment的状态
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
//将缓存对应位置置空
mFragments.set(position, null);
//销毁实例
mCurTransaction.remove(fragment);
}
public Parcelable saveState() {
}
public void restoreState(Parcelable state, ClassLoader loader) {
...
f.setMenuVisibility(false);
...
}
需要注意的是当视图重建时:
- ViewPager离屏缓存的Fragment,FragmentManager会帮助我们恢复
- 从来没有add到过FragmentManager中实例,我们在getItem放法中创建就好
- 曾经add到过Fragment实例,要保留之前的视图状态,依靠的是saveState | restoreState
其中restoreState会强制把 setMenuVisibility 置为false
结论
新框架中TabItemFragment的setMenuVisibility异常是由于:
- 新框架使用了 FragmentTabHost
- FragmentTabHost使用了 FragmentManager#attachFragment|detachFragment
- 终触发moveToState(
- 顶级Fragment的performCreateView、onViewCreated、performActivityCreated
- 其中performActivityCreated会遍历嵌套的Fragment去调用moveToState函数
- moveToState中会触发 mInnerView的restoreState.
- f.mView会在onDestroyView之前自动保存view树状态,并且在(同一个实例)onActivityCreate之后、onStart之前自动恢复之前保存的状态;
- 对于FeedTabContainerFragment,会触发ViewPager的onRestoreInstanceState
- 内部调用mAdapter.restoreState(ss.adapterState, ss.loader)
- FragmentStatePagerAdapter 会强制setMenuVisibility(false)
- moveToState中会触发 mInnerView的restoreState.
问题查明后,我们的解决方案其实也出来了,FragmentStatePagerAdapter恢复视图状态时只强制调用了setMenuVisibility(false),并没有影响setUserVisibleHint,因此将相关的判断改为setUserVisibleHint即可
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (getUserVisibleHint()) {
//界面可见
} else {
//界面不可见 相当于onpause
}
}
参考文献
- Android开发:ViewPage详细使用教程
- 嵌套Fragment的使用及常见错误
- FragmentStatePagerAdapter restore 的坑
- fragment的状态保存&恢复过程分析
- 当Fragment遇上ViewPager
- 神奇的viewPager预加载功能