面試博弈:掰扯5分鐘View的生命週期

面試就是一次技術的博弈過程,能唬住面試官就是勝利。如果每當面試官提出一個問題,都能掰扯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. 關聯面試題舉例:

  1. View的生命週期(整體或單個的)
  2. 自定義 View 的工作流程
  3. View 與 Activity 生命週期間的關係
  4. View,Window,Activity 的關係
  5. 事件分發機制
  6. View 中的內存泄漏
  7. 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章