1. 程式人生 > >Android View原理解析之基礎知識(MeasureSpec、DecorView、ViewRootImpl)

Android View原理解析之基礎知識(MeasureSpec、DecorView、ViewRootImpl)

提示:本文的原始碼均取自Android 7.0(API 24)

前言

自定義View是Android進階路線上必須攻克的難題,而在這之前就應該先對View的工作原理有一個系統的理解。本系列將分為4篇部落格進行講解,本文將主要對MeasureSpec、DecorView、ViewRootImpl等基礎知識進行講解。相關內容如下:

檢視原始碼的方式

既然要探究View的工作原理,閱讀原始碼自然是最好的手段了。其實在AndroidStudio中就可以很方便地查閱到Android系統的相關原始碼,但是在實際閱讀過程中會發現很多類和方法都是被隱藏的(@hide註解),嚴重影響學習熱情。

Github上已經有開發者上傳了去除@hide註解的原始碼jar包,我們只需要下載相應版本的Android.jar檔案,並且替換本地SDK目錄下相應版本的jar檔案,就可以很清爽地閱讀原始碼了。需要替換的路徑為:<SDK location>/platforms/android-xx/Android.jar

Github的地址如下:

該解決方案的原文地址如下:

注意:這種方式建議只在研究時使用,否則在開發過程中可能會在無意中引用@hide標記的API。

MeasureSpec

MeasureSpec可以翻譯為測量規格,主要用於View的測量過程,封裝著View的大小和測量模式(size和mode)。根據Android官方的註釋,MeasureSpec表達的是父容器對子View的一種佈局要求,可以簡單理解為對子View大小的限制。不過MeasureSpec並非是由父容器獨立決定的。實際上,父容器會通過自身的MeasureSpec結合子View的LayoutParams,共同生成子View的MeasureSpec,這一點在後續文章中會詳細講到。

關於LayoutParams的知識可以參考這篇部落格:

為了減少測量過程中建立物件的開銷,在實際使用中MeasureSpec以int(32位數)的形式存在,其高2位用於儲存mode,其餘位用於儲存size。系統提供的MeasureSpec類是View的靜態內部類,主要提供幾種實用的靜態方法,用於對int形式的MeasureSpec進行打包和解包,關鍵原始碼如下:

/**
 * 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.
 */
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 測量模式:父容器對子View的大小不作任何限制 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 測量模式:父容器已經為View指定了一個精確的大小 public static final int EXACTLY = 1 << MODE_SHIFT; // 測量模式:父容器為View指定了一個最大的大小 public static final int AT_MOST = 2 << MODE_SHIFT; /** * 根據傳入的size和mode建立一個MeasureSpec(打包) */ public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * 根據傳入的MeasureSpec獲取mode(解包) */ public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * 根據傳入的MeasureSpec獲取size(解包) */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }

MeasureSpec在View的測量流程中有著很重要的作用,這裡只是簡單介紹一下,在後續的文章中會結合具體原始碼進一步講解。

DecorView

回想我們在Activity中經常使用的方法setContentView,不知道大家有沒有想過我們設定的View被新增到了哪裡。由於在Android中只有ViewGroup的派生類才可以新增子View,那麼可以自然地想到,整個檢視樹(ViewTree)的根節點必定是一個ViewGroup,而DecorView就是ViewTree最頂級的容器。

實際上,DecorView只不過是FrameLayout的一個子類。PhoneWindow負責將其例項化,並根據當前Activity的風格特性(theme、feature、flag)為其新增不同的子佈局。但是無論使用什麼子佈局,子佈局中都會存在一個id為content的FrameLayout。我們平時在Activity中使用setContentView方法設定的View,就會被新增到這個名為content的FrameLayout中。這一過程發生在PhoneWindow#generateLayout中,關鍵程式碼如下:

protected ViewGroup generateLayout(DecorView decor) {
	..........
	
	// Inflate the window decor.
	// ① 根據Feature使用不同的佈局資原始檔
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        layoutResource = R.layout.screen_progress;
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogCustomTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_custom_title;
        }
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
            layoutResource = a.getResourceId(
                    R.styleable.Window_windowActionBarFullscreenDecorLayout,
                    R.layout.screen_action_bar);
        } else {
            layoutResource = R.layout.screen_title;
        }
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        layoutResource = R.layout.screen_simple;
    }

    mDecor.startChanging();
   
    // ② 在這個方法中解析佈局資原始檔(mDecor即DecorView)
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 

	// ③ 這裡的contentParent就是指id為content的那個FrameLayout
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    ..........
    
    mDecor.finishChanging();
    
    return contentParent;
}

可以看到,在程式碼①的位置根據不同的Feature為layoutResource賦值,其實就是在選擇DecorView需要使用的子佈局資原始檔。在程式碼②的位置呼叫了mDecor的onResourcesLoaded方法,這裡的mDecor指的就是DecorView。在這裡方法中會根據①中獲取的佈局資源進行解析,並將解析獲得的View新增到DecorView中。

在程式碼③的位置根據ID_ANDROID_CONTENT獲取了名為contentParent的ViewGroup,這其實就是setContentView方法設定的View的父容器。在Activity中我們也可以通過android.R.id.content獲得這個ViewGroup。

上面說了這麼多,其實就是想說明一件事,那就是DecorView是整個檢視樹的根容器。後續文章要講到測量、佈局、繪製流程,就是從DecorView開始向下傳遞的。

ViewRootImpl

ViewRootImpl開啟測量、佈局和繪製流程

ViewRootImpl實現了ViewParent介面(定義了View的父容器應該承擔的職責),處於檢視層級的頂點,實現了View和WindowManager之間必需的協議。拋開這些複雜定義,我們要知道的是ViewRootImpl將負責開啟整個檢視樹的測量-佈局-繪製流程。這一過程體現在ViewRootImpl的performTraversals方法中,這個方法將依次呼叫performMeasureperformLayoutperformDraw方法完成上述流程,關鍵程式碼如下:

private void performTraversals() {
    // mView是與ViewRootImpl繫結的DecorView
    final View host = mView;
    
    if (host == null || !mAdded)
        return;

    mIsInTraversal = true;
    boolean newSurface = false;
    WindowManager.LayoutParams lp = mWindowAttributes;

    // 這裡是螢幕的寬度和高度
    int desiredWindowWidth;
    int desiredWindowHeight;
    
    ........
    
    if (mFirst || windowShouldResize || insetsChanged ||
            viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
        ........

        if (!mStopped || mReportNextDraw) {
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||updatedConfiguration) {
       
            	// ① 獲取測量需要的MeasureSpec
            	int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                 // ② 開啟測量流程
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                // ③ 測量流程結束後就可以獲得DecorView的寬高了
                int width = host.getMeasuredWidth();
                int height = host.getMeasuredHeight();
                boolean measureAgain = false;

                if (lp.horizontalWeight > 0.0f) {
                    width += (int) ((mWidth - width) * lp.horizontalWeight);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }
                if (lp.verticalWeight > 0.0f) {
                    height += (int) ((mHeight - height) * lp.verticalWeight);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }

                if (measureAgain) {
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                layoutRequested = true;
            }
        }
    } else {
        maybeHandleWindowMove(frame);
    }
    
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
    	// ④ 開啟佈局流程
        performLayout(lp, mWidth, mHeight);

        // 計算透明區域
        ........
    }
    ........
    
    // Remember if we must report the next draw.
    if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
        reportNextDraw();
    }

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }
        // ⑤ 開啟繪製流程
        performDraw();
    } else {
    	........
    }

    mIsInTraversal = false;
}

可以看到,首先在程式碼①的位置通過getRootMeasureSpec方法獲得了測量DecorView需要的MeasureSpec,這也是整個檢視樹(ViewTree)最初的MeasureSpec,關鍵程式碼如下:

/**
 * 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;
}

在這個方法中將根據DecorView的佈局引數生成不同的MeasureSpec,windowSize即螢幕的寬/高,dimension則是DecorView中LayoutParams的width/height。可以看到在佈局引數為MATCH_PARENT或設定了具體寬/高(比如20dp這種形式)的情況下,生成的MeasureSpec都是使用EXACTLY模式(精確模式),否則使用AT_MOST模式。

隨後,在程式碼②的位置開啟了測量流程,程式碼④的位置開啟了佈局流程,程式碼⑤的位置開啟了繪製流程。

在performMeasure中將呼叫DecorView的measure方法進行測量:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    try {
        // 開始測量(mView就是DecorView)
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        ......
    }
}

在performLayout中將呼叫DecorView的layout方法進行佈局,並傳入測量完成後獲得的寬高:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    mLayoutRequested = false;
    mInLayout = true;

    final View host = mView;
    if (host == null) {
        return;
    }

    try {
    	 // 開始佈局(host就是DecorView)
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        mInLayout = false;
    } finally {
        ......
    }
    mInLayout = false;
}

在performDraw方法中將呼叫ViewRootImpl的draw方法,之後又會呼叫drawSoftware方法,最終將呼叫DecorView的draw方法開啟繪製流程:

/**
 * ViewRootImpl#performDraw
 */
private void performDraw() {
    if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
        return;
    } else if (mView == null) {
        return;
    }

    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    mFullRedrawNeeded = false;

    mIsDrawing = true;
    try {
    	 // 開始繪製
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
    }
    ......
}
/**
 * ViewRootImpl#draw
 */
private void draw(boolean fullRedrawNeeded) {
	 ......
	 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
         return;
     }
}
/**
 * ViewRootImpl#drawSoftware
 */
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    // Draw with software renderer.
    final Canvas canvas;
    ......

    try {
        canvas.translate(-xoff, -yoff);
        if (mTranslator != null) {
            mTranslator.translateCanvas(canvas);
        }
        canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
        attachInfo.mSetIgnoreDirtyState = false;

        // 呼叫DecorView的draw方法開始繪製
        mView.draw(canvas);

        drawAccessibilityFocusedDrawableIfNeeded(canvas);
    } finally {
    	......
    }
    return true;
}

ViewRootImpl的生成過程

上面講了ViewRootImpl的作用,這裡再提一下ViewRootImpl的生成過程:

首先是ActivityThread類的handleResumeActivity方法,在這裡會呼叫WindowManager的addView方法新增DecorView,關鍵程式碼如下:

ActivityThread#handleResumeActivity

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    r = performResumeActivity(token, clearHide, reason);

    .......
    
    if (r != null) {
        final Activity a = r.activity;
        
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.