前言:之前项目中也会遇到一些头疼的问题或者难解的bug,有些可能花费不少时间精力解决了,但是没有记录,打算从本篇博客开始,记录下项目中遇到的我认为的疑难杂症,算是对自己学习的总结,如果凑巧你也看到了,并且对你有一定的帮助,那将会是一件很有意义的事情。
最近同事做Tv项目,遇到了界面上一个奇怪的并且百思不得其解的现象,请我帮忙分析解决,先看下面的一个出现问题的效果图
进去注册页面,每个控件的值不完全一样,然后退出,再次进入注册页面,每个控件的值竟然完全一样。
一、问题初步排查定位思路:
1、代码逻辑错误导致数据保存一样?
2、fragment中的页面没有销毁?
首先很快速的排除了数据源是一样的问题,因为每个控件来源的数据值都是不一样的。
再看看fragment的生命周期,是否是页面没有销毁,下面是打印日志。
D/SIPFragment: onCreate
D/SIPFragment: onCreateView
D/SIPFragment: onDestroyView
D/SIPFragment: onDestroy
从日志中可以明显看到页面是销毁了,那下次进来页面应该是重新创建的啊,怎么会值是一样的呢?
二、问题再次查找
因为同事的fragment创建的时候使用是单例?我隐隐觉得和这个有点关系,遂把fragment每次创建的时候都new出新的对象,果然,再次进入页面,就是正常的现象了。
但是新的问题又出现了,同事反馈说,之前他也用过fragment单例,也没有出现过这个问题啊,这个就有点奇怪了,我的注意点又来到了自定义的控件上,因为里面的每个item是frameLatout控件封装,里面包含了EditText。
我顺着这个思路,去掉封装的控件,改用在xml中定义每个需要保存数据的EditText,重新运行程序,结果果然如预期的一样,是正常的,不是出问题的现象。
好了,到这里先简单理一下思路:
Q1、fragment单例会引起这个现象,但不能说单例一定会造成这个问题,单例是如何造成这个bug的呢?其中的机制是什么?
Q2、自己封装的控件,也会引起这个问题,这个又是怎么回事呢,虽然是自定义的控件,但是每个控件的对象和EditText的对象都是不一样的。
但与此同时,又提出了两个问题:
Q3、为什么是修改页面上最后一个控件的值,被保存下来了,换句话说,为什么每个控件相同的值是上一次页面最后一个控件的值?
Q4、为什么每个控件左边的TextView可以做到不相同,而EditText确是一样的呢?
我决定还是一如既往的从源码入手,毕竟我知道一切答案都可以从源码中找到。
既然单例会导致bug,那单例肯定出了问题,单例,顾名思义,是反反复复进入,对象还是同一个,我以经验判断,必定是对象里面的一些状态,标志位等数据没有恢复初始化状态,遂去源码中这个具体的地方查找。
/**
* Called by the fragment manager once this fragment has been removed,
* so that we don't have any left-over state if the application decides
* to re-use the instance. This only clears state that the framework
* internally manages, not things the application sets.
*/
void initState() {
mIndex = -1;
mWho = null;
mAdded = false;
mRemoving = false;
mFromLayout = false;
mInLayout = false;
mRestored = false;
mBackStackNesting = 0;
mFragmentManager = null;
mChildFragmentManager = null;
mHost = null;
mFragmentId = 0;
mContainerId = 0;
mTag = null;
mHidden = false;
mDetached = false;
mRetaining = false;
mLoaderManager = null;
mLoadersStarted = false;
mCheckedForLoaderManager = false;
}
找到了这个方法,是恢复初始状态的,感觉考虑的挺周到的嘛~,重置了这么多的变量,但我还是不放心,于是和变量声明的地方一一去比对,果然,被我发现了漏网之鱼,哈哈,真是看到了希望,这个变量就是mSavedViewState
SparseArray mSavedViewState;
这个变量看起来应该是缓存view数据状态的,我们看下其怎么使用的
case Fragment.ACTIVITY_CREATED:
if (newState < Fragment.ACTIVITY_CREATED) {
if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);
if (f.mView != null) {
// Need to save the current view state if not
// done already.
if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null){
//保存fragment的的view状态
saveFragmentViewState(f);
}
}
f.performDestroyView();
看到fragment在销毁前保存了状态,看下saveFragmentViewState方法。
void saveFragmentViewState(Fragment f) {
if (f.mView == null) {
return;
}
//如果数据为空,new一下
if (mStateArray == null) {
mStateArray = new SparseArray();
} else {
//有数据,清楚掉
mStateArray.clear();
}
//调用view的保存状态方法,将数据集合作为参数
f.mView.saveHierarchyState(mStateArray);
if (mStateArray.size() > 0) {
//如果有数据,将数据赋值给fragment中的数据缓存集合
f.mSavedViewState = mStateArray;
mStateArray = null;
}
}
我们继续往下看,有种逐渐明朗的感觉有没有哈。
View的saveHierarchyState方法
public void saveHierarchyState(SparseArray container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
//获取数据,调用onSaveInstanceState方法,稍后再看
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);
// 如果获取到数据,保存到SparseArray,key是View的ID。
container.put(mID, state);
}
}
}
好了,到这边能解释我们Q4了,这里面用控件的ID去作为key,我们知道无论是SprseArray还是HashMap,相同的key只会保存一个值,所以这就能解释为什么页面记录的是最后一个控件了。
我们继续看onSaveInstanceState方法,来看看能不能解释Q2,Q3的问题,EditText是TextView的子类,这个方法被TextView重载了,去看下其实现
TextView ->onSaveInstanceState
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
final boolean freezesText = getFreezesText();
//初始化是否有光标
boolean hasSelection = false;
int start = -1;
int end = -1;
if (mText != null) {
//内容不空,记录start,end位置
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}
// EditText有内容的话,hasSelection为true,会走这个分支
if (freezesText || hasSelection) {
//创建SavedState 这个子类
SavedState ss = new SavedState(superState);
if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}
ss.text = sp;
} else {
// 将文本内容放进缓存对象
ss.text = mText.toString();
}
}
ss.error = getError();
if (mEditor != null) {
ss.editorState = mEditor.saveInstanceState();
}
//EditText 返回
return ss;
}
//TextView返回
return superState;
}
这个方法的作用是保存EditText中的内容到缓存对象中,如果是TextView不会保存,好了,到这里就能解释TextView与EditText之间在缓存数据方面的差别了。
我们再次回到上面的dispatchSaveInstanceState方法
protected void dispatchSaveInstanceState(SparseArray 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);
}
}
}
数据怎么缓存的,看完了,那在哪里恢复的呢?怎么恢复的?我们继续看,
//省略部分代码
f.performActivityCreated(f.mSavedFragmentState);
// 此时已经调用完onCreateView,mView不为空
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
在performActivityCreated生命周期后开始恢复数据,这里就有意思了,这个地方调用是在onCreateView之后,设置数据是在onCreateView开始调用的initData里面,所以即使我给了一个正确的值,还是会被这个地方给刷掉,有意思吧
继续看restoreViewState方法。
final void restoreViewState(Bundle savedInstanceState) {
//由于是单例,之前的对象没有销毁,所以mSavedViewState 不为空。
if (mSavedViewState != null) {
//调用view的恢复数据的方法,这里就不继续往下看了
mInnerView.restoreHierarchyState(mSavedViewState);
//恢复完数据,重置该变量
mSavedViewState = null;
}
mCalled = false;
onViewStateRestored(savedInstanceState);
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onViewStateRestored()");
}
}
好了,到这里,这个问题,总算可以完结了,不过fragment为什么要保存view的状态呢,那是因为防止我们的进程被系统杀死,所以保存了fragment中所有需要保存的状态,只是我们这里错误的使用单例,导致了这个机制的触发,真是触目惊心啊。