请尊重原创劳动成果,转载请注明出处: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
这回还是要从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)方法。
onSaveInstantceState例子
下面把一个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方法。
View中的onSaveInstantceState和id的关系
好了,说了一大堆,貌似还没有进入本文关注的焦点。。。下面就来了:
上面说了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.
意思是说:Window类只有一个实现类,那就是PhoneWindow。
这下明白了,我们再去看看PhoneWindow类的源码,这个类我们并不能直接使用,它位于:Android源码目录/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
找到saveHierarchyState()方法,我们来看看它干了些什么事:
/** [email protected]} */ @Override public Bundle saveHierarchyState() { Bundle outState = new Bundle();//新建一个Bundle对象,用于存放状态 if (mContentParent == null) { return outState; } SparseArray<Parcelable> states = new SparseArray<Parcelable>();//新建了一个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状态 SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>(); savePanelState(panelStates); if (panelStates.size() > 0) { outState.putSparseParcelableArray(PANELS_TAG, panelStates); } if (mActionBar != null) {//保存actionBar状态 SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>(); 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(SparseArray<Parcelable> container) { dispatchSaveInstanceState(container); }
再来看看dispatchSaveInstanceState方法:
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 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; }
源码中写的很清楚: Some old apps reused IDs in ways they shouldn't have. Don't break them, but they don't get scroll state restoration.
如果你在应用中使用两个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,非允许请勿用于商业或盈利用途。
- 3楼u0116060633小时前
- 写的很详细,多谢
- 2楼vispin4小时前
- 学习到了新get
- 1楼ben0612昨天 23:44
- 好高级,从来没想过这样的问题