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的地址如下:

https://github.com/anggrayudi/android-hidden-api

该解决方案的原文地址如下:

https://www.jianshu.com/p/7c1aca9f001b

注意:这种方式建议只在研究时使用,否则在开发过程中可能会在无意中引用@hide标记的API。

MeasureSpec

MeasureSpec可以翻译为测量规格,主要用于View的测量过程,封装着View的大小和测量模式(size和mode)。根据Android官方的注释,MeasureSpec表达的是父容器对子View的一种布局要求,可以简单理解为对子View大小的限制。不过MeasureSpec并非是由父容器独立决定的。实际上,父容器会通过自身的MeasureSpec结合子View的LayoutParams,共同生成子View的MeasureSpec,这一点在后续文章中会详细讲到。

关于LayoutParams的知识可以参考这篇博客:

Android 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.getAttributes();
            a.mDecor = decor;

            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    // 调用WindowManager的addView方法添加DecorView
                    wm.addView(decor, l);
                } else {
                    .......
                }
            }
        } 
    }
    .......
}

上述代码中的wm是一个WindowManagerImpl对象(实现WindowManager),它的addView方法如下:

WindowManagerImpl#addView

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

mGlobal是一个WindowManagerGlobal对象,这里调用了它的addView方法,关键代码如下:

WindowManagerGlobal#addView

public void addView(View view, ViewGroup.LayoutParams params,
		Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

    .......

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
    	.......

        // ① 实例化一个ViewRootImpl
        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 {
			// ② 建立ViewRootImpl和DecorView的联系(view就是DecorView)
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            .......
        }
    }
}

在代码①的位置实例化了一个ViewRootImpl对象,然后在代码②的位置通过setView方法建立了ViewRootImpl和DecorView的联系,这里的view其实就是DecorView。

小结

本文简单讲解了在学习View的工作原理中需要知道的基础知识。如果看完后觉得不是很懂也没有关系,现在先对它们有个大致印象,然后结合后续的文章一起理解,效果可能会更好。

参考资料

https://blog.csdn.net/a553181867/article/details/51477040
https://blog.csdn.net/singwhatiwanna/article/details/50775201

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值