Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 一個Activity的顯示過程總結(四)

一個Activity的顯示過程總結(四)

編輯:關於Android編程

上一篇博客我們講到了ViewRoot中與UI相關的三個重要步驟:performMeasure(測量)、performLayout(布局)和performDraw(繪制),這次我們就來重點研究一下這三個方法。先上圖說明三個方法的關系:

measure、layout、draw

measure流程

在performTraversals中有多次measure的流程,我們只分析其中一次即可: (android.view.ViewRootImpl)
    final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
    private void performTraversals() {
    	...
        WindowManager.LayoutParams lp = mWindowAttributes;
				...
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    ...

                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                   ...
    }

首先我們構造了一個WindowManager.LayoutParams對象:mWindowAttributes,其中包含了有關於Window(最外層布局)的信息。在performTraversals中,我們把它賦值給了lp,並通過getRootMeasureSpec方法返回了兩個關鍵的測量量。我們一起來看看getRootMeasureSpec方法: (android.view.ViewRootImpl)
    /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

由該方法的注釋我們可以得知這個方法是用來確定rootView(即DecorView)的measure spec(一個測量量)的。該方法傳入兩個參數,第一個是window的實際大小,第二個是window的尺寸(一般而言是MATCH_PARENT,即占滿整個屏幕)。首先我們一起來看看MeasureSpec是什麼東西: (android.view.View)
    /**
     * A MeasureSpec encapsulates the layout requirements passed from parent to child.
     * Each MeasureSpec represents a requirement for either the width or the height.
     * A MeasureSpec is comprised of a size and a mode. There are three possible
     * modes:
     * UNSPECIFIED
     * The parent has not imposed any constraint on the child. It can be whatever size
     * it wants.
     * EXACTLY
     * The parent has determined an exact size for the child. The child is going to be
     * given those bounds regardless of how big it wants to be.
     * AT_MOST
     * The child can be as large as it wants up to the specified size.
     *
     * MeasureSpecs are implemented as ints to reduce object allocation. This class
     * is provided to pack and unpack thesize, mode tuple into the int.
     */
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        ...
    }

MeasureSpec是View內部的一個靜態類。根據其注釋我們可以得知:MeasureSpec用於描述通過父類到子類的布局要求(子類布局與父類相關),每個MeasureSpec表示寬度或高度的要求,一個MeasureSpec由一個大小(size)和模式(mode)組成(其實就是一個int,32位,根據計算方法可知前2位表示mode,後30位表示size)。定義的模式有三種: UNSPECIFIED:父類對子類沒有任何約束EXACTLY:父類為子類定義了一個確切大小AT_MOST:子類大小最大為傳入的size
makeMeasure方法的作用就是通過計算組合出一個合理的MeasureSpec。   回到getRootMeasureSpec,由於我們的Window默認是MATCH_PARENT,充滿屏幕大小的,因此getRootMeasureSpec返回的MeasureSpec為:size是屏幕的寬、高,mode是EXACTLY。在獲取了Window的MeasureSpec後,我們在performTraversals方法中調用了performMeasure方法,並把Window的MeasureSpec作為參數傳入: (android.view.ViewRootImpl)
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

該方法調用了mView(即DecorView)的measure,由於View中的measure是個final方法,因此DecorView調用的方法即是View的measure方法: (android.view.View)
    int mPrivateFlags;
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        // 大致是強制需要測量的意思
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    }

首先在View中出現了一個很重要的變量:mPrivateFlags,這是一個int類型的變量,它的每一個bit用於表示一種狀態。這個變量在研究layout與draw方法時我們也能見到。 在measure中,如果當前的狀態為需要強制測量,而傳入的MeasureSpec又不等於舊值時,就會調用onMeasure方法。onMeasure方法是我們自定義View時候可以重寫的方法(不能重寫final方法measure),在重寫onMeasure方法時有一點需要注意:我們需要在onMeasure方法中調用setMeasuredDimension方法設置寬與高,否則在第20行就會拋出IllegalStateException異常。 View提供的默認onMeasure實現就調用了setMeasuredDimension方法: (android.view.View)
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

總而言之,經過了measure流程,View的寬與高的大小就確定了。  

layout流程

measure流程確定了View的大小,接下來的layout流程就要確定View的位置了,在performTraversals中我們調用了performLayout方法: (android.view.ViewRootImpl)
    private void performTraversals() {
    		...
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
    }

(android.view.ViewRootImpl)
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ...

        final View host = mView;
        ...
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            ...
    }

在performLayout中,我們調用了host(即DecorView)的layout方法,由於ViewGroup類重寫了layout,我們來看看ViewGroup的layout方法: (android.view.ViewGroup)
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

貌似挖掘不了什麼有用的信息,我們繼續看看super調用的View的layout方法: (android.view.View)
    public void layout(int l, int t, int r, int b) {
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        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);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ...
        }
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    }

該方法首先調用setFrame方法查看View的大小布局與上次相比是否發生變化,如果發生變化或mPrivateFlags的狀態為需要進行layout,則調用onLayout進行布局。我們先來看看setFrame方法(setOpticalFrame內部也是通過setFrame方法完成): (android.view.View)
    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;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            ...


            if (sizeChanged) {
                if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) {
                    // A change in dimension means an auto-centered pivot point changes, too
                    if (mTransformationInfo != null) {
                        mTransformationInfo.mMatrixDirty = true;
                    }
                }
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            ...
        }
        return changed;
    }

setFrame通過比較left、right、top、bottom四個變量確定View的布局是否發生變化,並返回該布爾值。另外,如果setFrame通過計算發現View的大小也發生了變化,則會調用sizeChange方法: (android.view.View)
    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        if (mOverlay != null) {
            mOverlay.getOverlayView().setRight(newWidth);
            mOverlay.getOverlayView().setBottom(newHeight);
        }
    }

sizeChange會調用onSizeChanged,我們可以重寫該方法執行一些View大小變化時的操作。   回到layout方法,setFrame的返回值會被存在changed變量中,當changed為true時,即當View的布局發生了變化時,layout方法會調用onLayout方法。onLayout一般由View的子類進行重寫以執行一些布局操作,ViewGroup把onLayout重寫為抽象方法,使得每一個ViewGroup布局都需要重寫onLayout實現自己的特定布局效果: (android.view.ViewGroup)
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

draw流程

layout流程完成後,我們獲得了View的大小和布局,剩下的工作就是把View繪制到我們的屏幕上了。在performTraversals中,我們調用了performLayout獲取布局後,調用了performDraw來繪制我們需要的圖像: (android.view.ViewRootImpl)
    private void performDraw() {
        ...

        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        ...
    }

在performDraw方法中,我們調用了draw方法進行繪制,並傳入了一個布爾值表示是否整個屏幕都需要重新繪制: (android.view.ViewRootImpl)
    private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        ...

        final Rect dirty = mDirty;
        ...

        if (fullRedrawNeeded) {
            attachInfo.mIgnoreDirtyState = true;
            dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        }

        ...

        // 使用硬件渲染繪制
        if (!dirty.isEmpty() || mIsAnimating) {
            if (attachInfo.mHardwareRenderer != null && attachInfo.mHardwareRenderer.isEnabled()) {
                ...

                attachInfo.mHardwareRenderer.draw(mView, attachInfo, this,
                        animating ? null : mCurrentDirty);
            } else {
                ...

                // 使用軟件渲染繪制
                if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
                    return;
                }
            }
        }

        ...
    }

在draw方法中,我們首先根據傳入的布爾值計算出一個dirty的矩形區域(髒區域,表示要繪制的區域),然後使用硬件或軟件的方法進行渲染繪制,由於博主對硬件渲染不熟悉,這裡我們分析軟件方式渲染繪制的drawSoftware方法: (android.view.ViewRootImpl)
    /**
     * @return true if drawing was succesfull, false if an error occurred
     */
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        Canvas canvas;
        try {
            int left = dirty.left;
            int top = dirty.top;
            int right = dirty.right;
            int bottom = dirty.bottom;

            canvas = mSurface.lockCanvas(dirty);

            ...

        try {
            ...

                mView.draw(canvas);

            ...
        } finally {
            try {
                surface.unlockCanvasAndPost(canvas);
            } 
            ...
        }
        return true;
    }

在drawSoftware方法中,第15行首先調用mSurface(Surface對象,管理一塊用於繪制的緩存區)的lockCanvas方法,通過傳入dirty變量(髒區域)鎖定獲取了一塊畫布(Canvas對象),然後調用了mView(即DecorView)的draw方法在canvas上進行繪制,最後再使用mSurface的unlockCanvasAndPost方法解鎖提交畫布,交給底層進行渲染。雖然View的子類重寫了draw方法,但他們都調用了super.draw,因此我們接下來一起看看最關鍵的View的draw方法: (android.view.View)
    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) {
            ...
                    background.draw(canvas);
            ...
        }

        // Step 2, save the canvas' layers
        ...

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ...

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);

        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
    }

draw的注釋解釋的非常清晰,其過程主要分為6個步驟: 繪制背景:調用background.draw繪制背景保存布局為漸變准備繪制View本身:調用onDraw繪制子View:調用dispatchDraw繪制漸變效果並回復布局繪制裝飾品(如滾動條等) 當我們需要為自定義的View繪制時,只需重寫onDraw方法即可。   以上即是一個Activity的顯示過程的簡略總結,其中還有許多細節沒有研究,希望以後有時間可以去進一步深入探索。  
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved