一、解析Activity的构成
1、DecorView的创建
当我们调用startActivity
方法时,最终调用ActivityThread#handleLaunchActivity
,该方法中会首先会调用Activity的onCreate
方法。在onCreate
方法中,会调用Activity#setContentView
,setContentView
内部会调用Activity的成员变量mWindow的(Window是抽象类,其实现类是PhoneWindow,mWindow是PhoneWindow的一个实例)setContentView。其setContentView方法中,首先new一个DecorView对象,然后DecorView对象会根据不同的情况(主题,Window的feature等)加载不同的布局资源。DecorView是Activity中的根View,继承了FrameLayout。至此DecorView创建完成。
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绘制到屏幕上。
ViewRootImpl#performTraversals
会依次调用performMeasure
、performLayout
和performDraw
三个方法,这三个方法分别开启顶级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提供了measureChildWithMargins
和measureChildren
方法。
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)
方法的逻辑整理出如下表格:
表中的 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)。