基於源碼分析 Android View 繪製機制

在 Android 的知識體系中,View 扮演者很重要的角色。View 是 Android 在視覺上的呈現。本文結合 android-28 的源碼來分析 View 的繪製過程。java

ViewRootImpl

ViewRootImpl 類是鏈接 WindowManagerDecorView 的紐帶,View 的繪製流程均是經過 ViewRootImpl 來完成的。android

// ActivityThread.java

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
        
        ...
        
        if (r.window == null && !a.mFinished && willBeVisible) {
            // 獲取 WindowManager 及 DecorView
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    // 將 DecorView 添加到當前 Window 中
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }

        ...
    }
複製代碼

上面的代碼說明,在 ActivityThread 中,當 Activity 對象被建立完畢後,會將 DecorView 經過 WindowManager 添加到 Window 中。app

// WindowManagerImpl.java

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
複製代碼

能夠知道最終是經過 WindowManagerGlobaladdView 方法來將 DecorView 添加到 Windowide

// WindowManagerGlobal

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

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            // 初始化 ViewRootImpl 並將 ViewRootImpl 對象和 DecorView 創建關聯
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

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

上述代碼建立了 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 創建關聯。最終在 setView 方法中,會執行 ViewRootImplrequestLayout 方法來執行 View 的繪製流程函數

// ViewRootImpl.java

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

scheduleTraversals 方法最終會調用 performTraversals 方法,通過 measurelayoutdraw 三個過程才能最終將一個 View 繪製出來佈局

  • meausre: 用來測量 View 的寬和高
  • layout: 用來肯定 View 在父容器中的放置位置
  • draw: 負責將 View 繪製在屏幕上

如圖所示,performTraversals 會依次調用 performMeasureperformLayoutperformDraw 三個方法,這三個方法分別完成頂級 Viewmeasurelayoutdraw 這三大流程。其中在 performMeasure 中會調用 measure 方法,在 measure 方法中又會去調用 onMeasure 方法,在 onMeasure 方法中又會對全部的子元素進行 measure 過程,這個時候 measure 流程就從父容器傳遞到了子元素中了,這樣就完成了一次 measure 過程。接着子元素會重複父容器的 measure 過程,如此反覆就完成了整個 View 樹的遍歷。經過 performLayoutperformDraw 的傳遞流程跟 performMeasure 相似ui

MeasureSpec

爲了更好地理解 View 的測量過程,咱們還須要理解 MeasureSpecMeasureSpec 參與了 Viewmeasure 過程,在很大程度上決定了一個 View 的尺寸規格,但父容器也會影響 ViewMeasureSpec 的建立過程。在測量過程當中,系統會將 ViewLayoutParams 根據父容器所設置的規則轉換成對應的 MeasureSpec,而後再根據這個 MeasureSpec 來測量出 View 的寬和高。this

// View.java

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

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;

        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);
            }
        }

        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }
        
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }
複製代碼

MeasureSpec 表明一個 32 位的 int 值,高 2 位表明 SpecMode, 低 30 位表明 SpecSizespa

  • SpecMode: 測量模式
  • SpecSize: 在某種測量模式下的規格大小

MeasureSpec 經過將 SpecModeSpecSize 打包成一個 int 值來避免過多的對象內存分配makeMeasureSpec 是打包方法,getModegetSize 則爲解包方法。code

SpecMode 有三類,每一類都標識特殊的含義

UNSPECIFIED

父容器不對 View 有任何限制,要多大給多大,這種狀況通常用於系統內部,標識一種測量的狀態

EXACTLY

父容器已經檢測出 View 所須要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式

AT_MOST

父容器指定了一個可用大小即 SpecSizeView 的大小不能大於這個值,具體是什麼值要看不一樣 View 的具體實現。它對應於 LayoutParams 中的 wrap_content

MeasupreSpec 和 LayoutParams

MeasureSpec 不是惟一由 LayoutParams 決定的,LayoutParams 須要和父容器一塊兒才能決定 ViewMeasureSpec,從而進一步決定 View 的寬和高。

對於 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 來共同肯定;對於普通的 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同局誒的那個,MeasureSpec 一旦肯定後, onMeasure 中就能夠肯定 View 的測量寬和高

DecorView 建立 MeasureSpec

對於 DecorView 來講,它的 MeasureSpec 建立過程是由 getRootMeasureSpec 方法來完成的

// ViewRootImpl.java

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT: // 精確模式,大小就是窗口大小
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT: // 最大模式,大小不定,可是不能超過窗口的大小
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default: // 精確模式,大小爲 LayoutParams 中指定的大小
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
複製代碼

經過上述代碼,DecorViewMeasureSpec 的產生過程就很明確了,具體來講其遵照以下規則,根據它的 LayoutParams 中的寬和高參數來劃分

  • LayoutParams.MATCH_PARENT: 精確模式,大小就是窗口大小
  • LayoutParams.WRAP_CONTENT: 最大模式,大小不定,可是不能超過窗口的大小
  • 固定大小(如 100dp): 精確模式,大小爲 LayoutParams 中指定的大小

ViewRootImplperformTraversals 方法中調用 getRootMeasureSpec 獲取到 childWidthMeasureSpecchildHeightMeasureSpec 後,會傳給 performMeasure 方法,最終調用 DecorViewmeasure 方法

普通 View 建立 MeasureSpec

對於普通 View 來講,即佈局中的 ViewViewmeasure 過程由 ViewGroup 傳遞而來,在 ViewGroupmeasureChildWithMargins 方法

// ViewGroup.java

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

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

measureChildWithMargins 方法通常會在自定義 Layout 組件的 onMeasure 方法中調用(如 FrameLayout, LinearLayout),來測量子元素的規格。在調用子元素的 measure 方法以前會先經過 getChildMeasureSpec 方法來獲得子元素的 MeasureSpec。經過上面代碼可知,子元素的 MeasureSpec 的建立和父容器的 MeasureSpec 和子元素自己的 LayoutParams 有關,此外還和 Viewmarginpadding 有關

// ViewGroup.java

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 父容器的 mode 和 size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        // 子元素可用大小
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製代碼

getChildMeasureSpec 函數主要的做用是根據父容器的 MeasureSpec 同時結合 View 自己的 LayoutParams 來肯定子元素的 MeasureSpec

View 採用固定寬和高的時候,無論父容器的 MeasureSpec 是什麼,ViewMeasureSpec 都是精確模式而且其大小遵循 LayoutParamas 中的大小。當 View 的寬和高是 match_parent 時,若是父容器的模式是精確模式,那麼 View 也是精確模式而且其大小是父容器的剩餘空間;若是父容器是最大模式,那麼 View 也是最大模式而且其大小不會超過父容器的剩餘空間。若是父容器是最大模式,那麼 View 也是最大模式而且其大小不會超過父容器的剩餘空間。當 View 的寬和高是 wrap_content 時,無論父容器的模式是精準仍是最大化,View 的模式老是最大化而且大小不能超過父容器的剩餘空間。

View 工做流程

相關文章
相關標籤/搜索