面試就是一次技術的博弈過程,能唬住面試官就是勝利。如果每當面試官提出一個問題,都能掰扯5分鐘,想必會給面試官一個不錯的印象。同時,我們也可以將面試官的問題向自己擅長的領域引導,進而在面試過程中起到正向的引導作用。
然而,面試過程中經常不知道該說什麼,又該從何說起。本文將串一下相關知識點。幫助你輕鬆湊夠5分鐘~~
今天的主題就是——View的生命週期。
1. 概覽
View的生命週期,其實在面試中並不常被問到,但是卻可以牽扯很廣的一部分知識,進可引出自定義View ,事件分發機制,退可談到 Activity ,Fragment 生命週期。可以說是很重要的一部分知識了。
來,開始掰扯~
文不如圖,希望一張圖可以幫助我們更好的記憶~
2. View生命週期相關方法
2.1 Constructors() - 構造方法
如果大家寫過自定義 View 的話,想必會都會很清楚,View 有四個構造函數。
一般大家都知道第一個構造方法是簡單的在代碼中new View 的時候調用的,第二個構造方法使用最廣泛,是對應的生成 xml 中定義的 View 的時候調用的。
剩下的兩個構造方法,大家瞭解的就比較少了。一般在自定義 View 的時候都會不加思索的按照固定的寫法。
- 第1 種 構造方法
// 從代碼創建視圖時使用的簡單構造函數。
// context視圖運行的上下文,它可以通過這個上下文運行訪問當前的主題、資源等。
public View(Context context) { ... }
- 第 2 種 構造方法
構造函數,該構造函數在從XML擴展視圖時調用。當從XML文件構造視圖並提供在XML文件中指定的屬性時,將調用此方法。這個版本使用默認樣式0,所以應用的屬性值是上下文主題和給定的AttributeSet中的屬性值。
在添加了所有子元素之後,將調用 onfinishinflation() 方法。
//@param context視圖運行的上下文,它可以通過這個上下文運行訪問當前主題、資源等。
//@param使用XML標記的屬性來擴展視圖。
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
- 第 3 種 構造方法
從XML執行填充,並從主題屬性應用特定於類的基礎樣式。View的這個構造函數允許子類在擴展時使用它們自己的基礎樣式。
eg. Button類的構造函數將調用這個版本的構造函數,並應用R.attr.buttonStyle
中的風格。
這允許主題的按鈕樣式修改所有的基本視圖屬性(特別是其背景)以及按鈕類的屬性。
相對於第2種,多提供了一種給 View 添加默認屬性的方式
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
- 第 4 種 構造方法
相對第3個構造函數就多了一個 defStyleRes ,其實就是多了一種提供 View 默認屬性的一種方式。這種方式更加的簡單,直接在代碼中傳入 R.style.XX 就可以了。如果沒有默認值的話就爲 0 。這個參數只有 defStyleAttr 爲 0 的時候纔會生效。
需要注意的是@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// ...
}
2.2 onFinishInflate()
該方法當View及其子View從XML文件中加載完成後觸發調用。
也就是會在Activity中調用setContentView之後就會調用onFinishInflate這個方法,這個方法就代表自定義控件中的子控件映射完成了,然後可以進行一些初始化控件的操作,就可以通過 findViewById 得到控件,得到控件之後進行一些初始化的操作。
當然在這個 方法裏面是得不到控件的高寬的 ,控件的高寬是必須在調用了onMeasure方法之後才能得到,而onFinishInflate方法是在setContentView之後、onMeasure之前
2.3 onVisibilityChanged()
該方法在當前View或其父控件 的可見性改變時被調用。如果View狀態不可見或者GONE,該方法會第一個被調用。
onVisibilityChanged是否調用,依賴於View是否執行過onAttachedToWindow方法。也就是View是否被添加到Window上。
2.4 onAttachedToWindow()
onAttachToWindow 當View被附着到一個窗口時觸發。
在Activity第一次執行完onResume方法後被調用。
即:onCreate -> onStart -> onResume -> onAttachedToWindow
注意:該方法只會調用一次
2.5 onMeasure()
onMeasure 確定View以及其子View尺寸大小時被調用。
自定義 View 中,最重要的三個方法就是 onMeasure(),onLayout(),onDraw() ,所以這部分很重要了。
源碼:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ...
onMeasure(widthMeasureSpec, heightMeasureSpec);
//...
}
- measure()
measure()這個方法由final來修飾,意味着不能夠被子類重寫。
其作用是:測量出一個View的實際大小,而實際性的測量工作,Android系統卻並沒有幫我們完成,而是交給了onMeasure(),所以我們需要在自定義View的時候按照自己的需求,重寫onMeasure方法。
而子控件又分爲view和viewGroup兩種情況,具體測量的流程如下面所示:
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
兩個參數 widthMeasureSpec 和 heightMeasureSpec 均爲 int 類型,看名字知道是跟寬和高有關係,但它們其實不是寬和高,而是由寬、高和各自方向上對應的模式來合成的一個值:其中,在int類型的32位二進制位中,31-30這兩位表示模式,0~29這三十位表示寬和高的實際值。
其中模式一共有三種,被定義在View類的內部類 View.MeasureSpec 中:
①UNSPECIFIED:表示默認值,父控件沒有給子view任何限制。------二進制表示:00
②EXACTLY:表示父控件給子view一個具體的值,子view要設置成這些值的大小。------二進制表示:01
③AT_MOST:表示父控件個子view一個最大的特定值,而子view不能超過這個值的大小。------二進制表示:10
2.6 onSizeChanged()
onSizeChanged( 當view的大小發生變化時觸發 )
該方法在Measure方法之後且測量大小與之前不一樣的時候被調用。
2.7 onLayout()
onLayout 在當前View需要爲其子View分配尺寸和位置時會被調用。
measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。
爲視圖及其所有子視圖分配大小和位置
- 這是佈局機制的第二階段。(首先是測量)。在這個階段,每個父進程調用它的所有子進程的layout來定位它們。這通常是使用存儲在measure pass()中的子度量來完成的。
派生類不應重寫此方法。帶有子元素的派生類應該重寫onLayout。在該方法中,它們應該調用每個子元素的layout。
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*/
public void layout(int l, int t, int r, int b) {
// ...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
//...
}
layout()方法接收四個參數,分別代表着左、上、右、下的座標,當然這個座標是相對於當前視圖的父視圖而言的。可以看到,這裏還把剛纔測量出的寬度和高度傳到了layout()方法中。
2.8 onDraw(Canvas)
onDraw 該方法用於View渲染內容的細節。
measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字就可以判斷出,在這裏才真正地開始對視圖進行繪製。
ViewRoot中的代碼會繼續執行並創建出一個Canvas對象,然後調用View的draw()方法來執行具體的繪製工作。
- draw()方法內部的繪製過程總共可以分爲六步,其中第二步和第五步在一般情況下很少用到,因此這裏我們只分析簡化後的繪製過程。代碼如下所示:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
- onDraw()函數中不允許創建任意變量,因爲當需要重繪時就會調用onDraw()函數,所以在onDraw()函數中創建的變量就會一直被重複創建,這樣會引起頻繁的程序GC(回收內存),進而引起程序卡頓。
一般在自定義控件的構造函數中創建變量,既在初始化時一次性被創建,開始繪製圖畫的過程中不要在繪製函數裏創建任意變量;
2.9 onWindowFocusChanged()
- 該方法也可能在繪製過程中被調用,具體是在包含當前View的Window獲得或失去焦點時被調用。此時可以設置代碼中定義的View的一些LayoutParameter。
- 當包含此視圖的窗口獲得或失去焦點時調用。請注意,這與視圖焦點是分開的:要接收鍵事件,視圖及其窗口都必須具有焦點。如果一個窗口顯示在你的窗口的頂部,接受輸入焦點,那麼你自己的窗口將失去焦點,但視圖焦點將保持不變。
- 如果View進入了銷燬階段,肯定是會被調用的。
2.10 onWindowVisibilityChanged()
該方法同上,具體是在包含當前View的Window可見性改變時被調用。
- 當包含的 window 可見性被改變時調用。請注意,這將告訴您 window 是否對 window manager可見;這並不能告訴您您的窗口是否被屏幕上的其他窗口所隱藏,即使它本身是可見的。
- 從onWindowFocusChanged被執行起,用戶可以與應用進行交互了,而這之前,對用戶的操作需要做一點限制。
2.11 onDetachedFromWindow()
onDetachedFromWindow
當View離開附着的窗口時觸發,比如在Activity調用onDestroy方法時View就會離開窗口。
注意:該方法與onAttachedToWindow 對應,同樣只會調用一次
3. View其它的一些生命週期相關方法
- onFocusChanged()
該方法在當前View獲得或失去焦點時被調用。
- onKeyDown()
該方法在有按鍵按下後被調用。
- onKeyUp()
與上面對應,該方法在有按鍵按下後彈起時觸發。
- onTrackballEvent()
該方法在一個軌跡球運動事件發生時被調用。
- onTouchEvent()
該方法在觸屏事件發生時被調用。
- onSaveInstanceState()
這個方法就不用說了,在Activity被Pause的時候被調用。被Pause後回到界面時View就沒方法被調用了。只有在比如Activity被銷燬時進入View的銷燬流程。
4. 總結:
-
View 的關鍵生命週期爲 [改變可見性] --> 構造View --> onFinishInflate --> onAttachedToWindow --> onMeasure --> onSizeChanged --> onLayout --> onDraw --> onDetackedFromWindow
-
在Activity的onCreate方法中加載View,View的onFinishInflate會被調用,繼而Activity的生命週期執行到onResume方法之後View才被附着到窗口上,繼而進行繪製工作,onMeasure、onSizeChanged 、onLayout、onDraw。這幾個方法可能由於setVisible或onResume被調用多次,最後是Window失去焦點後的銷燬階段。
-
onVisibilityChanged()方法在View是可見狀態時如上所示時機調用,但是View的狀態如果是不可見或者GONE時,是首先被調用的。如果是Invisible狀態,View的創建到layout即結束,不會繪製出來。如果是GONE狀態,View也會被加載並添加到Window,但是不會再Measure、Layout和Draw了。也就時說即使是GONE狀態,銷燬時一樣有Detach的過程,即View的銷燬過程和可見性無關。
-
創建和銷燬流程設置可見性區別:
visibility和Invisibitlity差距只有invisibility不需要繪製view(ondraw)
visibitlity和gone差距是gone不需要測量大小(onmeasure)、不需要給子類分配尺寸(onlayout)、不需要繪製view(ondraw)。
Invisibility和gone的差距是gone不需要測量大小(onmeasure)、不需要給子類分配尺寸(onlayout)
5. 關聯面試題舉例:
- View的生命週期(整體或單個的)
- 自定義 View 的工作流程
- View 與 Activity 生命週期間的關係
- View,Window,Activity 的關係
- 事件分發機制
- View 中的內存泄漏
- View 的性能優化
…
6. 相關推薦博文:
Android View生命週期 https://blog.csdn.net/u013353866/article/details/48597251
深入理解android view 生命週期 https://blog.csdn.net/sun_star1chen/article/details/44626433
View生命週期流程圖 https://blog.csdn.net/yangshuaionline/article/details/91993532
Android開發——View的生命週期總結 https://blog.csdn.net/SEU_Calvin/article/details/72855537
Android自定義控件系列七:詳解onMeasure()方法中如何測量一個控件尺寸(一) http://doc.okbase.net/cyp331203/archive/140383.html
Android視圖繪製流程完全解析,帶你一步步深入瞭解View(二) https://blog.csdn.net/guolin_blog/article/details/16330267