學習筆記-淺析Android繪製流程

1、 Activity層級結構

Activity要作的事情比較多,爲了解耦以及複用,將外觀和行爲的管理放到window中,讓window負責view的建立管理以及與viewRootImpl的交互。java

img

  • PhoneWindowwindow的惟一實現。
  • DecorViewwindow的頂層視圖,包含窗口裝飾。繼承自FrameLayout,是一個視圖真實的根。和window互相持有對方。
  • ContentParentDecorView的子view,放置窗口內容視圖。便可以認爲是DecorView自己,也能夠認爲它是DecorView的用來存放內容視圖的子級。

當在Activity中經過setContentView()設置視圖的時候,會層層傳遞到PhoneWindow.setContentView()中,經過addView()或者inflate()添加到ContentParent中。canvas

public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (...) {
        ...
    } else {
        mContentParent.addView(view, params);
    }
    ...
}
複製代碼

2、 繪製起點

  • DecorView是最頂層的View,是視圖樹的的根,整個視圖樹的繪製從這裏開始。安全

  • DecorView的繪製由ViewRootImpl控制,ViewRootImpl.performTraversals()前後調用了performMeasure()peformLayout()performDraw(),在其中,分別對應調用了DecorViewmeasure()layout()draw()方法,進行對應的繪製流程。markdown

    private void performTraversals() {
        ...
        performMeasure();
        ...
        performLayout();
        ...
        performDraw();
    }
    
    private void performMeasure() {
        ...
        mView.measure();
    }
    
    private void performLayout() {
        ...
        mView.layout();
    }
    
    private void performDraw() {
        ...
        mView.draw();
    }
    複製代碼
  • ViewRootImpl工做的起點是在ViewRootImpl.setView()被調用的時候。ViewRootImpl.setView()主要的工做是:佈局

    1. DecorView存入ViewRootImplpost

    2. 調用requestLayout()啓動繪製。this

    3. ViewRootImpl做爲parent存入DecorView,完成相互的綁定。spa

    void setView(View view,...) {
        if (mView == null) {
            mView = view;
            ...
            requestLayout();
            ...
            view.assignParent(this);
        }
    }
    複製代碼
  • ViewRootImpl.addView()調用鏈:線程

    • ActivityThread.handleResumeActivity()
    • WindowManagerImpl.addView()
    • WindowManagerGlobal.addView(),在其中建立ViewRootImpl並調用ViewRootImpl.setView()

    根據這個調用鏈能夠知道,resume以後纔開始進行繪製流程,因此以前view的寬高並無計算出來,讀取不到view的寬高。另外,在resume以前的操做並不必定真正的繪製出來。code

  • ViewRootImpl.requestLayout()調用的時候,不是當即開始繪製,而是將回調綁定到下一幀,在指定的時刻開始回調到performTraversals(),開始繪製。另外能夠看到ViewRootImpl.requestLayout()中才進行了線程檢查,這就解釋了在ViewRootImpl被初始化以前,不會進行線程安全的檢查,即在初始化以前在其餘線程對view的數據進行更改可能不會報錯。這也是onCreate()中不在主線程更新view數據不會報錯的緣由。

    public void requestLayout() {
        if (...) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    
    
    void scheduleTraversals() {
        if (...) {
            ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
    複製代碼
  • View.requestLayout()遞歸調用parent.requestLayout(),最終遞歸到ViewRootImpl.requestLayout()中,從新進行繪製,只會走measurelayout流程。

    public void requestLayout() {
        ...
        if (...) {
            mParent.requestLayout();
        }
    }
    複製代碼
  • View.invalidate()遞歸調用parent.invalidateChild(),最終遞歸到ViewRootImpl.invalidateChild()中從新繪製,只會走draw流程。遞歸時傳遞了重繪的位置,只會對調用的view的位置進行重繪。

    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
        ...
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }
    }
    複製代碼
  • ViewGroup.addView()中前後調用了requestLayout()invalidate(),從新進行繪製。前面說過在setContentView()的時候,是經過addView()進行添加,可是這個調用的時間在resume以前,這個時候ViewRootImpl尚未初始化,DecorViewparent還不是VIewRootImpl,因此並不會開始繪製。

    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        ...
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }
    複製代碼

3、 繪製流程

繪製流程分紅三個步驟:measurelayoutdraw

1. measure流程

  • measure過程當中,會層層傳遞widthMeasureSpecheightMeasureSoecMesureSpecView內的一個靜態類,封裝了父級傳遞到子級的佈局要求,每一個MeasureSpec表示了對高度寬度寬度的要求。使用32位int來表示,高2位表示測量模式,低30位表示測量尺寸。測量模式有三個:

    • USPECIFIED:00,父佈局不對子級施加任何約束。
    • EXACTLY:01,父佈局已經肯定子級的大小,好比match_parent或者xxxdp。
    • AT_MOST:10,自適應,子級在父佈局給定的範圍內肯定本身的大小。
  • MeasureSpec中還封裝了幾個方法,方便對於MeasureSpec的建立以及拆解。

  • public static class MeasureSpec {
        public static int makeMeasureSpec(int size, int mode) {
            ...
            return size + mode;
        }
    
        public static int getMode(int measureSpec) {
            ...
        }
    
        public static int getSize(int measureSpec) {
            ...
        }
    }
    複製代碼
  • DecorView.measure()執行的是View.measure()方法,能夠看到這是個final方法,也就是說全部view的measure都是走的這裏,在標誌了強制刷新(forceLayout)或者尺寸並未被正確測量(needsLayout)的狀況下,會調用onMeasure()進行測量。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        if (forceLayout || needsLayout) {
            ...
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
    複製代碼
  • 對於不是ViewGroupView,在onMeasure()中只須要測量自身便可,通常會調用View.onMeasure(),在其中計算大小,並調用setMeasureDimension()儲存測量的寬高。

    在getDefaultSize()計算寬高的時候,對於AT_MOST和EXACTLY都返回父類要求的最大大小,即wrap_contentmatch_parent取的計算結果都至關於match_parent,若是直接繼承View實現自定義view的時候,須要注意處理這種狀況。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);
    
        switch (specMode) {
            case View.MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case View.MeasureSpec.AT_MOST:
            case View.MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }
    複製代碼
  • Decorview.onMeasure()執行的是FrameLayout.onMeasure()方法。主要的步驟是:

    • 首先遍歷全部子view,調用ViewGroup.meaureChildWithMargins()測量子view大小,並取得子view中最大的寬高,做爲自身可實現要求的最小寬高。
    • 然後調用resolveSizeAndState()與父級傳遞的佈局要求綜合計算得到最終寬高。
    • 計算出自身的寬高以後,若是設置match_parent的子view超過一個,會從新對這些view進行計算寬高,由於這些view寬高受到FrameLayout的寬高的影響,以前的計算結果可能並不正確。
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (...) {
                measureChildWithMargins();
                final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                ...
                if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
                        lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
    
            }
        }
        ...
        setMeasuredDimension(resolveSizeAndState(maxWidth, ...),
                resolveSizeAndState(maxHeight, ...));
        
        count = mMatchParentChildren.size();
        if (count > 1) {
            ...
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
    複製代碼
  • ViewGroup.measureChildWithMargins(),主要有兩個工做

    • 結合父類的佈局要求以及自身的設置計算須要傳遞到子級的佈局要求
    • 調用子view的measure()

    到這裏就完成了一次完整的遞歸,又調用到了子view的measure(),以後再繼續根據view類型繼續向下傳遞,完成整個view樹的計算。

    protected void measureChildWithMargins(...) {
        ...
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    複製代碼

2. layout流程

  • Decorview.layout()執行的是ViewGroup.layout(),主要調用的是super的方法,也就是View.layout()

    public final void layout(int l, int t, int r, int b) {
        if (...) {
            ...
            super.layout(l, t, r, b);
        }
    }
    複製代碼
  • View.onLayout()首先調用setFrame(),判斷對其父view的位置是否發生改變,根據是否發生改變調用onLayout()

    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 || ...) {
            onLayout(changed, l, t, r, b);
            ...
        }
    }
    
    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        ...
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
    
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            ...
        }
        return changed;
    }
    複製代碼
  • onLayout()主要完成對子view的從新佈局,對於不是ViewGroupView不包含子View,調用View.onLayout(),是個空方法;而ViewGroup.onLayout()abstract,須要不一樣的佈局管理器實現不一樣的佈局方法。

  • DecorView會執行到FrameLayout.onlayout(),計算出子view的位置信息,並調用子view的layout()方法。

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }
    
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        ...
        for (int i = 0; i < getChildCount(); i++) {
            ...
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                            lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
    
            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                            lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }
    
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
            
        }
    }
    複製代碼

3. draw流程

  • Decorview重寫了draw()方法,調用父類方法,也就是View.draw(),以後作了菜單背景的繪製。

    public void draw(Canvas canvas) {
        super.draw(canvas);
    
        if (mMenuBackground != null) {
            mMenuBackground.draw(canvas);
        }
    }
    複製代碼
  • View.draw(),官方註釋了繪製步驟:

    1. 繪製背景
    2. 保存圖層,準備繪製漸變邊緣(陰影等效果)
    3. 繪製自身
    4. 繪製子view
    5. 繪製漸變邊緣,恢復圖層
    6. 繪製裝飾(類如滾動條)
  • 其中2和5只在須要的狀況下才會執行,主要的繪製流程是1,3,4。

  • public void draw(Canvas canvas) {
        ...
        // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    		...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
    		...
        // Step 4, draw the children
        dispatchDraw(canvas);
    		...
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
        ...
    }
    複製代碼
  • View.drawBackgroud(),背景不爲空的狀況下,最終調用Drawable.draw()完成背景繪製。

    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        ...
        background.draw(canvas);
    }
    複製代碼
  • DecorView也重寫了ondraw(),在其中也是先調用父類方法,而後繪製了自身,View.onDraw()是個空方法,每一個view內容各不相同,須要對應的去實現各自的繪製方法,就好比DecorView.ondraw(),在其中繪製後備背景,完成了自身的繪製。

    public void onDraw(Canvas c) {
        super.onDraw(c);
    
        mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
                mStatusColorViewState.view, mNavigationColorViewState.view);
    }
    複製代碼
  • DecorView.dispatchDraw()執行的是ViewGroup.dispatchDraw(),在其中遍歷子view並調用drawChild()方法。對於不是ViewGroupView來講,自己就沒有子view,這個方法就沒有什麼意義,因此View.dispatchDraw()是個空方法。

    protected void dispatchDraw(Canvas canvas) {
        ...
        for (int i = 0; i < childrenCount; i++) {
            ...
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || ...) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ...
    }
    
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    複製代碼
  • ViewGroup.drawChild()會調用到View中另外一個有boolean返回值draw()方法(View中有兩個draw()方法),其中仍是會調用到沒有返回值的draw()方法。官方註釋解釋,這個有booleandraw()方法,只是提供給ViewGroup.drawChild()使用,這個方法的主要工做是根據圖層類型(CPU or GPU)進行渲染和硬件加速的地方。

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        if (...) {
            ...
            draw(canvas);
        }
        ...
    }
    複製代碼
  • 到這裏完成了draw一次遞歸,最終完成整個樹的繪製。

相關文章
相關標籤/搜索