基於原始碼分析 Android View 繪製機制

騎摩托馬斯發表於2019-01-24

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

ViewRootImpl

ViewRootImpl 類是連線 WindowManagerDecorView 的紐帶,View 的繪製流程均是通過 ViewRootImpl 來完成的。

// 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 中。

// WindowManagerImpl.java

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

可以知道最終是通過 WindowManagerGlobaladdView 方法來將 DecorView 新增到 Window

// 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 繪製在螢幕上

基於原始碼分析 Android View 繪製機制

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

MeasureSpec

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

// 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 位代表 SpecSize

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

MeasureSpec 通過將 SpecModeSpecSize 打包成一個 int 值來避免過多的物件記憶體分配makeMeasureSpec 是打包方法,getModegetSize 則為解包方法。

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 的模式總是最大化並且大小不能超過父容器的剩餘空間。

基於原始碼分析 Android View 繪製機制

View 工作流程

相關文章