总结" />
当前位置: 代码迷 >> Android >> 总结
  详细解决方案

总结

热度:652   发布时间:2016-04-28 01:15:42.0
【Android面试】(二):你不能不知道的view---加id和不加id的区别?

请尊重原创劳动成果,转载请注明出处: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;


可能你已经在猜测这个window和PhoneWindow的关系了,Window是一个抽象类,其中的setContentView方法也是一个抽象方法,并没有实现。来看看Window类的注释:


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
好高级,从来没想过这样的问题
  相关解决方案