Android自定義View之Window、ViewRootImpl和View的三大流程

xxq2dream發表於2018-08-10

Android自定義View系列

View的三大流程指的是measure(測量)、layout(佈局)、draw(繪製)。

下面我們來分別看看這三大流程

View的measure(測量)

MeasureSpec

MeasureSpec是View的一個內部靜態類

//view.class
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    ...

    /**
     * 這種模式不用關心
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * 精確模式,對應的是match_parent和具體值,比如100dp
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * 最大模式,對應的就是wrap_content
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

   
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * 獲取測量的模式
     */
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    /**
     * 獲取測量到的尺寸大小
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    ...
}
複製程式碼

MeasureSpec總結起來就是:

  • 它由2部分資料組成,分別為定義了View測量的模式和View的測量尺寸大小
  • 其中EXACTLY精確模式表示的是match_parent和具體值;AT_MOST最大模式表示的是wrap_content的情況

View的measure過程

View的measure過程由其measure方法完成,在measure方法中會呼叫View的onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼

setMeasuredDimension方法會設定View的測量寬高,所以我們知道getDefaultSize方法返回的就是View的測量寬高。我們來看看getDefaultSize方法

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    //對應的是wrap_content
    case MeasureSpec.AT_MOST:
    //對應的是match_parent和具體值,返回的是測量值
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
複製程式碼

從getDefaultSize方法我們可以看到,無論是測量模式無論是AT_MOST還是EXACTLY,返回的結果都是specSize這個測量後的大小。當View的測量模式是AT_MOST,也就是我們在佈局中給View設定的是wrap_content時,這個specSize實際上是父容器中的可用大小,也就相當於是和match_parent是一樣的效果了。所以我們在通過繼承View來自定義View時,就需要特別處理wrap_content的情況。

ViewGroup的measure過程

對於ViewGroup來說,除了完成自己的測量,還需要完成子元素的測量。ViewGroup是一個抽象類,為了測量子類,它提供了一個measureChildren方法:

//ViewGroup.class
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    
    //用子元素的LayoutParams構建MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼

可以看出ViewGroup的measureChildren方法最終會迴圈呼叫子元素的measure方法完成子元素的測量。

ViewGroup並沒有定義自己的測量過程,因為它的測量過程要由子類自己完成,比如LinearLayout和RelativeLayout,顯然測量過程是不同的。有興趣的可以看看LinearLayout的onMearsure方法。

常見的在Activity中獲取View的寬高的方法

View的measure過程和Activity生命週期方法是不同步的,需要用特殊的方法才能準確獲取View的寬高

(1)onWindowFocusChanged方法中獲取

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);

    if (hasFocus) {
        int width = myView.getMeasuredWidth();
        int height = myView.getMeasuredHeight();
    }
}
複製程式碼

需要注意的是onWindowFocusChanged方法會被呼叫多次

(2)view.post(runnable)

通過view的post方法可以將一個runnable投遞到訊息佇列的尾部,當Looper呼叫此runnable時,View已經初始化好了

myView.post(new Runnable() {
    @Override
    public void run() {
        int width = myView.getMeasuredWidth();
        int height = myView.getMeasuredHeight();
    }
});
複製程式碼

(3)ViewTreeObserver

利用ViewTreeObserver的OnGlobalLayoutListener回撥介面,當View樹發生狀態改變時會回撥這個介面

ViewTreeObserver viewTreeObserver = myView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        myView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        int width = myView.getMeasuredWidth();
        int height = myView.getMeasuredHeight();
    }
});
複製程式碼

View的layout(佈局)

layout的作用就是ViewGroup用來確定子元素的位置,ViewGroup的位置被確定後,就會呼叫onLayout方法,遍歷所有的子元素並呼叫其layout方法,在layout方法中又會呼叫onLayout方法。layout方法確定View本身的位置,而onLayout方法用來確定子元素的位置。

//View.class
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    //確定View的四個頂點的位置
    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);

        ...
    }

    ...
}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
複製程式碼

layout方法主要就做了2件事,一個是呼叫setFrame方法確定自身的位置,另一個就是呼叫onLayout方法確定子元素的位置。

我們看到在View中並沒有實現onLayout方法,同樣的在ViewGroup中也沒有實現onLayout方法,這是因為onLayout的具體實現同樣和具體的佈局有關,所以需要子類根據具體情況去實現。大家有興趣可以看看LinearLayout的onLayout的實現。

需要注意的是預設情況下測量的寬高和最終的寬高是一樣的,也就是getMeasuredWidth和getWidth是一樣的。只不過一個獲取的是measure過程後得到的寬高,一個是layout過程後的寬高。所以如果measure過程需要進行多次或是認為改變了layout方法,就有可能2者不相等。不過絕大多數都是一樣的。


View的draw(繪製)

Draw說白了就是把View的內容繪製到螢幕上

@CallSuper
public void draw(Canvas canvas) {
    ...

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;
    
    //繪製背景
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    ...
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        //呼叫onDraw方法繪製內容
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        //呼叫dispatchDraw方法繪製子元素
        dispatchDraw(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        //繪製裝飾
        onDrawForeground(canvas);

        // we're done...
        return;
    }

    ...
}
複製程式碼

從上面的draw方法中,我們可以看出,繪製過程遵循如下幾步:

(1)繪製背景:drawBackground(Canvas canvas)

(2)繪製自身內容:onDraw(canvas)

(3)繪製子元素:dispatchDraw(canvas)

(4) 繪製裝飾:onDrawForeground(canvas)


View的三大流程開始的地方---ViewRootImpl

ViewRootImpl的performTraversals方法工作流程

上面這張圖是一張很經典的圖,很好的描述了View的繪製流程。ViewRootImpl中的performTraversals方法會呼叫performMeasure、performLayout、performDraw方法,開始View的測量、佈局和繪製過程。那ViewRootImpl中的performTraversals方法又是在什麼時候被呼叫的呢?這就需要理解一個視窗的概念,也就是Window。

Android中的Window

Window是一個抽象的概念,每一個Window都對應著一個View和ViewRootImpl,Window通過ViewRootImpl來和View建立聯絡。Android中所有的檢視都是通過都是通過Window來呈現的,不管是Activity、Dialog、還是Toast,它們的View都是附加在Window上的,因此Window實際上是View的直接管理者。比如我們觸控螢幕的事件,就是通過Window傳遞給DecorView,然後再由DecorView傳遞給我們的View。我們在Activity、Dialog中設定檢視內容的方法setContentView在底層也是通過Window來完成的

Window的新增過程

我們在啟動一個Activity或是一個Dialog時,系統都會為我們建立一個Window,並把建立的Window註冊到系統的WindowManagerService中。

Window的新增過程需要通過WindowManager的實現類WindowManagerImpl的addView方法來實現。只有在通過addView方法將View新增到Window中後,我們的View才和Window關聯起來,才能接收通過Window傳遞的各種輸入資訊

//WindowManagerImpl.class
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
複製程式碼

由WindowManagerImpl的原始碼我們發現,WindowManagerImpl把新增View的工作都交給了WindowManagerGlobal類處理。我們來簡單看看WindowManagerGlobal類

//WindowManagerGlobal.class
//所有Window所對應的View
private final ArrayList<View> mViews = new ArrayList<View>();
//所有Window對應的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//多有Window對應的佈局引數
private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
//正在被刪除的View
private final ArraySet<View> mDyingViews = new ArraySet<View>();

public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    ...

    //建立ViewRootImpl
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);
    
    //將Window的一系列物件新增到對應的列表中
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
        //呼叫ViewRootImpl的setView方法
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }
}
複製程式碼

原始碼中已經做了相應的註釋了。這裡我們看到WindowManagerGlobal在addView方法中建立ViewRootImpl後,最後呼叫了ViewRootImpl的setView方法。下面我們來看看ViewRootImpl的setView方法

//ViewRootImpl.class
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            ...
            //呼叫了requestLayout進行View的繪製
            requestLayout();
            ...
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                //這裡呼叫WindowSession的addToDisplay方法註冊Window
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {
                mAdded = false;
                mView = null;
                mAttachInfo.mRootView = null;
                mInputChannel = null;
                mFallbackEventHandler.setView(null);
                unscheduleTraversals();
                setAccessibilityFocus(null, null);
                throw new RuntimeException("Adding window failed", e);
            } finally {
                if (restore) {
                    attrs.restore();
                }
            }

            ...

        }
    }
}

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
複製程式碼

在ViewRootImpl的setView方法中主要是做了2件事,一個是呼叫requestLayout方法啟動View的繪製流程;另一個是呼叫WindowSession的addToDisplay方法請求WindowManagerService新增Window,這是一次IPC呼叫。

至此,我們就分析完Window的新增過程了,總結如下:

(1)View的展示以及處理觸控點選事件離不開Window,2者通過ViewRootImpl進行關聯

(2)我們在啟動Activity、建立Dialog或是彈出Toast時都會建立一個Window,然後會通過WindowManagerGlobal類的addView方法來建立ViewRootImpl類,並將Window、View、ViewRootImpl關聯起來,

(3)在建立完ViewRootImpl後,接著會呼叫ViewRootImpl的setView方法,在setView方法中通過requestLayout方法最終呼叫到performTraversals方法開啟View的三大流程;通過WindowSession的addToDisplay方法向WindowManagerService發起遠端IPC呼叫,完成Window的新增。

總結

(1)在通過繼承View的方式自定義View時,需要特別處理wrap_content的情況,因為View中預設相當於沒處理(和match_parent效果一樣)

(2)在Activity中獲取View的寬高需要用特殊的方式:onWindowFocusChanged、view.post(runnable)、ViewTreeObserver的OnGlobalLayoutListener

(3)我們的View的顯示離不來Window,無論是Activity、Dialog還是Toast,都對應著一個Window。View和Window通過ViewRootImpl來建立關聯。我們顯示、更新、隱藏介面,比如Dialog的show和dismiss,說到底是Window中新增、更新和刪除View的過程。

(4)我們通過setContentView方法新增View,其實是對應Window的新增View的過程,Window會建立ViewRootImpl來執行註冊Window、開啟View的繪製流程的操作。

(5)所以綜上,我們顯示一個介面的過程為:建立Window-->建立ViewRootImpl-->新增View-->繪製View、註冊Window


                    歡迎關注我的微信公眾號,和我一起每天進步一點點!
複製程式碼

AntDream

相關文章