【Android面試】(二):你不能不知道的view---加id和不加id的區別?,androidview---
請尊重原創勞動成果,轉載請註明出處: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()方法,我們來看看它幹了些什麼事:
/** {@inheritDoc} */ @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,非允許請勿用於商業或盈利用途。