Android读书笔记(4)—— View的工作原理

一、解析Activity的构成

1、DecorView的创建

当我们调用startActivity方法时,最终调用ActivityThread#handleLaunchActivity,该方法中会首先会调用Activity的onCreate方法。在onCreate方法中,会调用Activity#setContentViewsetContentView内部会调用Activity的成员变量mWindow的(Window是抽象类,其实现类是PhoneWindow,mWindow是PhoneWindow的一个实例)setContentView。其setContentView方法中,首先new一个DecorView对象,然后DecorView对象会根据不同的情况(主题,Window的feature等)加载不同的布局资源。DecorView是Activity中的根View,继承了FrameLayout。至此DecorView创建完成。

Android读书笔记(4)—— View的工作原理_第1张图片

2、添加DecorView到Window

完成DecorView的创建之后,接着调用ActivityThread#handleResumeActivity方法。在handleResumeActivity方法中,首先调用Activity#onResume方法,handleResumeActivity方法接着会得到一个DecorView对象和一个WindowManager对象(接口,实现类是WindowManagerImpl),然后调用WindowManagerImpl#addView方法,DecorView对象作为入参传入。在WindowManager#addView中,创建了一个ViewRootImpl对象(ViewRoot的实现类),并调用了ViewRootImpl#setView,DecorView对象作为入参。在ViewRootImpl#setView方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上,才能真正显示出来。在这个过程中,ViewRootImpl、DecorView和WMS会彼此关联,最后通过WMS调用ViewRootImpl#performTraverals方法开始View的测量、布局、绘制流程。

Window是一个抽象类,具体是实现是PhoneWindow,Activity、Dialog等的视图都需要附加到Window上来呈现。
WindowManager是外界访问Window的入口,实现类是WindowManagerImpl,Window的具体实现是在WindowManagerService中,WindowManager和WindowManagerService的交互是一个IPC过程。。
DecorView是顶级View,是一个FrameLayout布局,代表了整个应用的界面。内部有titlebar和contentParent两个子元素,contentParent的id是content,而我们设置的main.xml布局则是contentParent里面的一个子元素。
ViewRoot的实现类是ViewRootImpl,在WindowManager中创建,用于将DecorView添加到Window中。

二、理解MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。

//主要理解 & ~ | 位运算的作用,体会这样设计的妙处
public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//11000000 0000...000
        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(int size,int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
}

MeasureSpec通过将SpecSize和SpecMode打包成了一个int值来避免过多对象的内存分配。
SpecMode有三类:

UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部。
EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式(也不一定,还受父容器影响,详见下面的表格)。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,对LayoutParams中的wrap_content。
说明:上面描述的是理论上应该有的逻辑。

对于顶级DecorView,其MeasureSpec是由窗口尺寸和自身的LayoutParams共同确定。对于普通的View,其MeasureSpec由父容器和自身的LayoutParams共同确定。一旦MeasureSpec确定,onMeasure中就可以确定View的测量宽/高。

三、View的工作流程

主要指measure、layout、draw这三大流程。measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。

Android读书笔记(4)—— View的工作原理_第2张图片

ViewRootImpl#performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法,这三个方法分别开启顶级View的measure、layout和draw这三大流程。

其中performMeasure中会调用顶级View#measure 方法,measure调用onMeasure,在onMeasure 方法中则会测量自身并调用所有子元素measure方法,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View树的遍历。另外两个过程同理。

1、ViewGroup的Measure流程

对于ViewGroup既要测量自身,也要遍历子元素的measure方法(通过实现onMeasure方法)
在performMeasure方法中,调用了DecorView#measure(继承自View,其实调用的是View#measure),measure会调用onMeasure方法。ViewGroup并没有定义onMeasure,这个方法需要子类去实现,主要需要实现两个功能:①测量自身②测量子View。

ViewGroup提供了measureChildWithMarginsmeasureChildren方法。

1.1、measureChildWithMargins方法
protected void measureChildWithMargins(View child,
    int parentWidthMeasureSpec, int widthUsed,
    int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //入参:父容器的MeasureSpec;父的padding和自身的margin(剩下为子元素可用空间);自身的宽度。
    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);
    //注意:此时的入参是自身的MeasureSpec。measure又会调用child#onMeasure方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

从上面的方法可以看出,View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高。getChildMeasureSpec(int spec, int padding, int childDimension)方法的逻辑整理出如下表格:

Android读书笔记(4)—— View的工作原理_第3张图片

表中的 parentSize是指父容器目前可以使用的大小,即父容器的specSize减去入参padding

ViewGroup并没有定义onMeasure,需要其子类去实现,为什么ViewGroup不像View一样对其onMeasure做统一呢?因为不同的ViewGroup子类有不同的布局特征,导致测量细节各不相同,无法统一。

根据上面的表格,我们发现父容器的MeasureSpec属性为AT_MOST,子元素的LayoutParams为WRAP_CONTENT的时候,子元素的测量模式为AT_MOST,它的SpecSize为父容器的SpecSize减去padding(入参),也就是说子元素WRAP_CONTENT和MATCH_PARENT一样的。为了解决这个问题,需要在WRAP_CONTENT时指定一下默认的宽高。

1.2、measureChildren方法

measureChildren中会循环调用measureChild方法,在measureChild中,首先会调用getChildMeasureSpec方法,入参和上面类似,区别在于padding入参仅仅为自身的padding,然后会调用子元素的measure方法(和measureChildWithMargins非常类似)。

2、View的Measure过程

View的measure方法是一个final方法,会调用onMeasure方法,因此只需要关注onMeasure方法,入参为自己的measureSpec

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension用于设置测量的宽高,测量好之后,必须调用

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

简单理解,getDefaultSize返回的就是measureSpec中的specSize,这就是View测量后的大小。在AT_MOST和EXACTLY模式下,都返回了specSize。也就是说对于一个直接继承View的自定义View,它的wrap_content和match_parent效果一样,因此如果要实现自定义View的wrap_content,则要重写onMeasure方法。解决问题:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
      // 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
      //参考TextView、ImageView的处理方式
      //其他情况下沿用系统测量规则即可
    if (widthSpecMode == MeasureSpec.AT_MOST
            && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWith, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWith, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    }
}

getSuggestedMinimumWidth()方法就是:如果View没有设置背景,就返回minWidth属性值(可以为0);如果设置了背景,就返回minWidth和背景的最小宽度之间的最大值。

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过 getMeasuredWidth/Height 方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。一个较好的习惯是在onLayout方法中,去获取View测量宽高或最终宽高。

3、如何正确获得宽高

如果我们想要在Activity启动的时候就获取一个View的宽高,怎么操作呢?因为View的measure过程和Activity的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:

3.1、Activity/View#onWindowFocusChanged

onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。

3.2、view.post(runnable)

通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。

3.3、ViewTreeObserver

使用 ViewTreeObserver 的众多回调可以完成这个功能,比如OnGlobalLayoutListener 这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取View宽高的好时机。需要注意的是,伴随着View树状态的改变, onGlobalLayout 会被回调多次。

3.4、view.measure(int widthMeasureSpec,intheightMeasureSpec)

手动对view进行measure。需要根据View的layoutParams分情况处理:

  • match_parent:直接放弃。根据上表所示,需要知道parentSize,即父容器剩余空间,而此时无法知道这个值。
  • 具体的数值( dp/px):
  int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  view.measure(widthMeasureSpec,heightMeasureSpec);
  • wrap_content:
  int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  // View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  view.measure(widthMeasureSpec,heightMeasureSpec);

四、layout过程

layout方法确定View本身的位置,会调用onLayout方法。onLayout确定所有子元素的位置,通过遍历所有的子View并调用其layout方法。

View#layout中,setFrame确定View的四个顶点位置,即初始化mLeft,mRight,mTop,mBottom这四个值(确定了最终的宽高),也就确定了View在父容器中的位置。接着调用onLayout方法,确定所有子View的位置,和onMeasure一样,onLayout的具体实现和布局有关,因此View和ViewGroup均没有真正实现onLayout方法。

View的测量宽高和最终宽高的区别:
在View的默认实现中,View的测量宽高和最终宽高相等,只不过测量宽高形成于measure过程,最终宽高形成于layout过程。即便View需要多次测量才能确定自己的测量宽高,但最终来说,测量宽高和最终宽高还是一致。

五、draw过程

View的绘制过程遵循如下几步:

  • 绘制背景 drawBackground(canvas)
  • 绘制自己 onDraw
  • 绘制children dispatchDraw 遍历所有子View的 draw 方法
  • 绘制装饰 onDrawScrollBars

View#setWillNotDraw,如果一个View不需要绘制任何内容,那么置为ture,系统会进行相应的优化。默认情况下,View为false,ViewGroup为true。所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭 WILL_NOT_DRAW 这个优化标记位,即调用 setWillNotDraw(false)。

六、自定义View

你可能感兴趣的:(Android读书笔记(4)—— View的工作原理)