请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45313125,非允许请勿用于商业或盈利用途。
上次面试,Android开发,被问到:你知道android中,布局文件中加id和不加id有什么区别?这个我真的不知道,蒙了,只能硬着头皮说:加了id会在R文件中生成对应id的数值,然后扯了点view树,总之答非所问。。。虽然最后面试也过了,但是这个问题一直萦绕在心头,挥之不去。刚好今天复习Activity生命周期的时候,看到了相关知识点。
有关Activity的onSaveInstantceState(Bundle outState)方法的一些基础知识在上篇文章中有提到过,大家可以去看看:【Android面试】(一):Android中activity保存状态和数据到底该在哪个方法中进行,必须承认,上篇文章中调侃的语气太重,如果有冒犯,提前说句抱歉,本人还是很尊重面试官的,毕竟肯定要比我强才来面试我。
这回还是要从activity中的onSaveInstantceState(Bundle outState)方法说起,下面快速的过一下onSaveInstantceState(Bundle outState)的几个要点:
1、onSaveInstantceState(Bundle outState)会在activity能够被销毁之前被调用,也就是所谓的(killble)状态,这个在上篇中有提到
2、onSaveInstantceState(Bundle outState)会在onStop()方法之前被调用,但不保证会在onPause()方法之前还是之后被调用。
3、重点!!!onSaveInstantceState(Bundle outState)不是一定会被调用的,什么时候会被调用呢?简单一句话:当Activity要进入这么一种状态:“系统可能会以非应用行为退出Activity方式干掉Activity”之前,系统就会调用onSaveInstantceState(Bundle outState)方法。
4、非应用行为退出
什么是非应用行为退出?应用行为退出Activity:比如主动调用finish()方法,或者主动按Back键,让Activity结束。非应用行为退出:比如一个Activity在后台,过了很长时间也没有被重新调用显示出来;又或者系统当前资源非常紧张,主动kill掉当前activity,释放资源以供其他应用使用。
这样设计的逻辑是很清晰的:当系统不确定会不会什么时候在未经“允许”的突发情况下结束掉Activity,在进入这种状态之前,肯定需要保存一下我们想要的数据,比如Activity中有控件有状态值,可以通过onSaveInstantceState(Bundle outState)进行保存,但是就像上一篇文章中说的,onSaveInstantceState(Bundle outState)不保证一定会被调用,因为它不是Activity生命周期中的方法。
5、假设onSaveInstantceState(Bundle outState)方法被调用了,且也保存了数据到Bundle对象,那么什么时候会将其取出来?
上面的第3点中提到过,在系统要进入可以使用“非应用行为”杀死Activity状态之前,会调用onSaveInstantceState(Bundle outState)方法,而Bundle对象可能被取到的条件,就是系统确实使用“非应用行为”杀死了Activity,而在要重建Activity时,会首先将Bundle对象传给onCreate,然后再传给onRestoreInstanceState(Bundle savedInstanceState)方法。如果onSaveInstantceState(Bundle outState)方法调用之后,Activity没有“意外杀死”,那么再次启动Activity时,只会调用onStart--onResume,而不会调用onRestoreInstanceState(Bundle savedInstanceState)方法。
下面把一个Activity在启动到被旋屏之后重新创建的过程打印结果展示出来,这里在onSaveInstantceState方法中往bundle中存一个当前时间,然后在onCreate方法和onRestoreInstanceState方法中将其取出,onCreate方法中会对Bundle进行判空:
启动:
旋屏之后:
下面给Activit中加两个按钮,让它点击跳转到第二个activity,不同的是,一个按钮会在点击时调用finish方法,而另一个则不会:
跳转调用finish()方法:
跳转不调用finish()方法:
发现在onCreate第一次调用时,Bundle为null,而在旋屏之后,onCreate和onRestoreInstanceState方法中都拿到了传过来的时间。
而在主动调用finish结束activity时,没有调用onSaveInstantceState方法;而如果不finish掉activity1,直接跳转activity2,则会在activity1的onStop之前调用onSaveInstantceState方法。
好了,说了一大堆,貌似还没有进入本文关注的焦点。。。下面就来了:
上面说了onSaveInstantceState方法,下面来看看这个方法里到底干了什么:(你mei的,怎么还是onSaveInstantceState方法?!汗Σ( ° △ °|||)︴,就快到了)
来看看Activity中的源码:
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
getApplication().dispatchActivitySaveInstanceState(this, outState);
}
1、mWindow.saveHierarchyState()中的数据,放入到Bundle对象中
2、将Fragments中的state数据存放到Bundle对象中
3、将Bundle对象通过Application的dispatchActivitySaveInstanceState进行分发。
今天本文关注第一个问题:mWindow.saveHierarchyState()
发现返回的是一个mWindow,而这个mWindow是一个Activity类中 Window类型的成员变量:
private Window mWindow;
The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window.
这下明白了,我们再去看看PhoneWindow类的源码,这个类我们并不能直接使用,它位于:Android源码目录/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
找到saveHierarchyState()方法,我们来看看它干了些什么事:
/** {@inheritDoc} */
@Override
public Bundle saveHierarchyState() {
Bundle outState = new Bundle();//新建一个Bundle对象,用于存放状态
if (mCOntentParent== null) {
return outState;
}
SparseArraystates = new SparseArray ();//新建了一个SparseArray,里面维护了一个key数组和一个value数组,类似于Map
mContentParent.saveHierarchyState(states);//调用view里面的saveHierarchyState方法,将状态值保存到
outState.putSparseParcelableArray(VIEWS_TAG, states);将SparseArray中的数据存储到Bundle对象中
// save the focused view id
View focusedView = mContentParent.findFocus();
if (focusedView != null) {
if (focusedView.getId() != View.NO_ID) {//注意这里,如果当前焦点view有设置id,才会进入到下面
outState.putInt(FOCUSED_ID_TAG, focusedView.getId());//特别存储当前焦点view的id值
} else {
if (false) {
Log.d(TAG, "couldn't save which view has focus because the focused view "
+ focusedView + " has no id.");
}
}
}
// save the panels 保存panel状态
SparseArraypanelStates = new SparseArray ();
savePanelState(panelStates);
if (panelStates.size() > 0) {
outState.putSparseParcelableArray(PANELS_TAG, panelStates);
}
if (mActionBar != null) {//保存actionBar状态
SparseArrayactiOnBarStates= new SparseArray ();
mActionBar.saveHierarchyState(actionBarStates);
outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates);
}
return outState;
}
PhoneWindow类的成员变量mContentParent用来描述一个类型为DecorView的视图对象,或者这个类型为DecorView的视图对象的一个子视图对象,用作UI容器.当它的值等于null的时候,就说明正在处理的应用程序窗口的视图对象还没有创建.
简而言之,只要我们给Activity设置了显示内容,不管是布局文件还是view,就会挂载在这个mContentParent之下。所以一般情况下,mContentParent不会为null
好了,上面其实已经看出来了一点东西,如果一个焦点view的id没有设置的话,那么就无法向Bundle对象中保存当前焦点view的id,焦点标识是使用的:FOCUSED_ID_TAG这个常亮。
我们再来看看mContentParent.saveHierarchyState(states)干了些什么:
由于:private ViewGroup mContentParent;是一个viewgroup,而viewGroup里没有saveHierarchyState方法,于是实际上调用的view中的saveHierarchyState方法:
public void saveHierarchyState(SparseArraycontainer) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArraycontainer) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {//只有有id的情况下才能进入到里面,添加view的状态
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState();//调用view自己的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);
}
}
}
看到这里基本上真相大白了,如果不给一个view设置一个id,那么在Activity调用onSaveInstantceState(Bundle outState)方法时,就没办法保存它的状态,而且即使它当前是焦点view,也没办法将其焦点状态记录在Bundle对象中,这会导致在需要取出Bundle状态对象时,出现问题。
上面还可以看到,view的状态,是由onSaveInstanceState()方法来获取的:
protected Parcelable onSaveInstanceState() {
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
return BaseSavedState.EMPTY_STATE;
}
1、实际上view默认的onSaveInstanceState方法中什么都没做,返回的是一个空状态,这个方法是一个protected方法,于是在view的各个子类中,可能会有不同的实现,因为毕竟不是每个view都需要保存状态,而且不同类型的view要保存的状态值也不近相同,比如textview可能需要保存文字状态,而scrollview就需要保存滚动到的位置值等等。
2、我们可以通过自定义View,重写onSaveInstanceState和onRestoreInstanceState方法,来保存和取出一些应对特定环境时比较重要的状态值。
3、值得注意的是,container.put(mID, state);状态值是由id的值来作为key值来存储的,所以如果同类的view,在使用相同的id时,在取状态值的时候,就可能会出现问题,来看看scrollview中的onSaveInstanceState方法:
@Override
protected Parcelable onSaveInstanceState() {
if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Some old apps reused IDs in ways they shouldn't have.
// Don't break them, but they don't get scroll state restoration.
return super.onSaveInstanceState();
}
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.scrollPosition = mScrollY;
return ss;
}
如果你在应用中使用两个ScrollView,且都指定一样的id,那么在onSaveInstanceState时,后调用的那个则会覆盖掉之前的那个ScrollView的Scroll的值,导致在之后取出的时候,会让两个ScrollView的滑动进度总是一样。
而且看上面的判断条件:if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2),表示在4.3(包括)以前ScrollView的scroll state是不会保存的,所以在这之前要实现对应的功能,只能自定义一个view继承ScrollView,然后重写onSaveInstanceState相关方法了。
好了,今天就到这里,谢谢大家!
请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45313125,非允许请勿用于商业或盈利用途。