Android View原理浅析——View的工作原理
Android View原理浅析——View的工作原理下图是Android的UI管理系统的层级关系。PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质...
下图是Android的UI管理系统的层级关系。
- PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
- DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
- ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等
ViewRoot与DecorView
ViewRoot是连接WIndowManager和DecorVIew的纽带。View的三大流程(measure layout draw)都是通过ViewRoot完成的。在ActivityThread中,Activity创建后,DecorView会被添加到Window中,同时创建ViewRootImpl,然后将DecorView与ViewRootImpl建立关联(通过ViewRoot的setView方法)。
View绘制从ViewRoot的performTraversals方法开始,经过measure、layout、draw三个过程,然后将View绘制出来
- measure方法用于测量View的宽高
- layout方法用于确定View在父容器中的位置
- draw方法负责将View绘制在屏幕上
如下是performTraversals的流程图:
它会依次调用performMeasure、performLayout、performDraw方法,分别完成顶级View的measure、layout、draw流程。performMeasure会调用measure方法,而measure又会调用onMeasure方法,在onMeasure方法中又会对子元素进行measure,这样重复下去就完成了整个View树的遍历。
performLayout、performDraw传递过程也非常类似,不过performDraw是在draw方法中通过dispatchDraw方法实现的。
measure过程决定了View的宽高,而Layout方法则确定了四个顶点的坐标和实际的宽高(往往等于measure中计算的宽高),draw方法则决定了View的显示。只有完成了draw方法才能正确显示在屏幕上。
MeasureSpec
MeasureSpec很大程度上决定了View的尺寸规格(父容器也会造成影响)。测量过程中,系统会将View的LayoutParams转换为MeasureSpec,然后通过MeasureSpec来测量View的宽高。
MeasureSpec是一个32位的int值,它的最高两位用来存放测量模式SpecMode,而后面的30位则用来存放规格大小SpecSize。
PS:这里用到了位运算进行状态压缩来节省内存。
SpecMode
SpecMode有三类,每一类都有特殊含义
- UNSPECIFIED:父容器不做任何限制,要多大给多大。通常应用于系统内部,表示一种测量状态
- EXACTLY:父容器已经检测出View需要的大小,此时View的大小就是SpecSize指定的值,对应了LayoutParams中的
match_parent
和指定具体数值两种情况 - AT_MOST:父容器指定了可用大小SpecSize,View的大小不能大于这个值,具体值取决于不同View的具体实现。对应了LayoutParams的
wrap_content
与LayoutParams的对应关系
下表是普通View的MeasureSpec的创建规则对应表
childLayoutParams / parentSpecParams | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSIze | EXACTLY childSIze | EXACTLY childSIze |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
View的工作流程
View的三大流程如下
- measure:测量,决定了View的宽度和高度
- layout:布局,决定了View最终的宽高及四个顶点的位置
- draw:绘制,将View绘制到屏幕上
measure过程
measure过程需要分为两类,普通的View通过measure方法后就完成了它的测量过程,而ViewGroup除了自己的测量过程外,还会遍历所有子元素的measure方法,子元素再递归执行,才能完成
View的measure过程
View的measure过程由measure方法来完成,measure方法是一个final方法,不能重写,它会调用VIew的onMeasure方法。onMeasure方法中会调用getDefaultSize方法,而getDefault方法中又会调用getSuggestedWidth和getSuggestedHeight方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
查看getDefaultSize方法可以看到下面的代码:
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方法所返回的就是测量后的View的大小。
我们接着看到getSuggestedWidth和getSuggestedHeight方法
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
它在没有指定background的情况下,返回的是minSize这一属性对应的值,而在指定了背景的情况下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法对应的值
这两个方法在Drawable有原始宽度的情况下返回原始宽度,否则返回0
从getDefaultSize方法可以看出,View的宽高由specSize决定。
因此,直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
想要解决这个问题,我们可以参考下面的代码。我们只需要给View指定一个默认宽高。在wrap_content时设置此宽高即可。而非wrap_content我们只需要沿用系统宽高即可。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widhtSpecSize = MEasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MEasureSpec.getSize(widthMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST &&
heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOSt){
setMeasuredDimension(mWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup的measure过程
ViewGroup除了完成自己的measure过程,还会遍历调用子元素的measure方法,然后子元素再次递归执行,ViewGroup是一个抽象类,因此没有重写View的onMeasure方法。但它提供了一个measureChildren的方法,如下
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
可以看到,ViewGroup执行measure时,会遍历子元素,调用measureChild方法对子元素进行measure。
下面是measureChild方法:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
它会取出子元素的LayoutParams,通过getChildMeasureSpec方法创建子元素MeasureSpec,然后传递给View的measure方法进行测量。
ViewGroup没有定义测量具体过程,因为它是个抽象类。具体的测量过程的onMeasure方法需要子类来实现,由于它的子类的特性可能会很大不同,所以没法做统一处理(如LinearLayout和RelativeLayout)。
下面我们研究一下LinearLayout的onMeasure实现:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
它根据横向和纵向分别调用measureVertical和measureHorizontal方法。我们可以查看measureVertical的一部分代码:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
...
}
...
}
会发现系统会遍历子元素并对每个子元素调用measureChildBeforeLayout方法。这个方法会调用子元素的measure方法。这样子元素就开始进入measure过程。并且系统会计算子元素的高度并存放在mTotalHeght中
子元素测量完毕后,LinearLayout会测量自己的大小:
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
对于竖直的LinearLayout来说,它在水平方向测量过程遵循View的测量过程,而竖直方向,如果在布局中采用match_parent或具体值,则和View的测量过程一致,如果采用的是wrap_content,则它的高度是所以子元素占用的高度的和。但仍然不会超过它的父容器的剩余空间。它的最终高度还需要考虑它在竖直方向的padding。
View的measure过程是十分复杂的,在measure完成后,通过getMeasuredWidth或getMeasuredHeight方法即可正确获得View的宽和高。
onCreate等方法中无法获取正确宽高的解决方法
注意,在某些极端情况下,系统可能需要多次measure才能拿到最终的测量宽高。此时的测量结果是不准确的。因此最好在onLayout中去获取View的测量宽高/最终宽高
我们有时候会想要在onCreate、onStart、onResume中获取View的宽高,但是其实是获取不到正确的宽高信息的。因为他们的生命周期与View的measure过程不是同步的,无法保证执行它们时View已经测量完毕。为了获取正确的宽高,可以用下面的方法来获取:
1. Activity/View 的 onWindowFocusChanged
onWindowFocusChanged中,VIew已经初始化完毕了,此时获取宽高是没问题的。
但需要注意的是,onWindowFocusChanged会被调用多次。当Activity窗口得到焦点和失去焦点时都会被调用。如果频繁进行onResume和onPause,则它会被频繁调用
2. View.post(Runnable)
通过post方法可以将Runnable投递到消息队列尾部,等待Looper调用后此Runnable时,View已经初始化完毕了
3. ViewTreeObeserver
用ViewTreeObserver的回调可以完成这个功能,如OnGlobalLayoutListener接口。当View树的状态改变或者内部的View可见性改变时,它都会被回调。这是获取View的宽高的很好的时机。
需要注意的是,随着View树的状态改变等,它会被调用多次
4. view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对View进行measure来得到宽高,比较复杂,具体可参考《Android开发艺术探索》P192
layout过程
Layout的作用是ViewGroup确定子元素的位置。当ViewGroup被确定后,在onLayout中会遍历所有子元素并调用layout方法,在layout方法中onLayout方法又会调用。layout方法确定View的位置,而onLayout方法则确定所有子元素的位置。
我们首先看View的layout方法:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
首先,通过setFrame方法设定View四个顶点的位置(初始化mLeft,mRight,mTop,mBottom)。四个顶点一旦确定,则在父容器中的位置也确定了,接着便会调用onLayout方法,来让父容器确定子容器的位置。onLayout同样和具体布局有关,因此View和ViewGroup均没有实现onLayout方法。
我们查看LinearLayout的onLayout方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
可以看到,和onMeasure的实现逻辑类似,我们进入layoutVertical
void layoutVertical(int left, int top, int right, int bottom) {
...
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
它会遍历所有子元素并调用setChildFrame来为子元素指定对应位置。其中childTop会逐渐增大,也就是后面的子元素会放置在靠下的位置。父元素在layout方法中完成自己的定位后,通过onLayout方法调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置。这样就完成了View树的layout过程。
我们查看setChildFrame的源码,看看它是怎么为子元素指定对应位置的。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
根据layoutVertical的代码可以看出,这里的width和height就是子元素的测量宽高。而在layout方法中会通过setFrame来设置子元素四个顶点位置。在setFrame方法中有如下几句赋值语句,就确定了子元素的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
getWidth和getMeasuredWidth的区别
我们看到getWidth的源码
public final int getWidth() {
return mRight - mLeft;
}
由于之前的mLeft、mRight都是由Measure过程赋值过来来看,它的返回值其实就是测量宽度。不过测量宽高形成在Measure过程,实际宽高形成于Layout过程。
在日常开发中,我们可以认为View的测量宽高与实际宽高是相等的。
但是如果我们故意重写了onLayout方法,可能就会导致两个宽高不等
public void layout(int l, iny t, int r, int b){
super.layout(l, t, r + 100,b + 100);
}
draw过程
draw过程是将View绘制到屏幕上,它遵循下面几步:
- 绘制背景 background.draw(canvas);
- 绘制自己 onDraw
- 绘制children(dispathDraw)
- 绘制装饰(onDrawScrollBars)
View绘制过程的传递是通过dispatchDraw实现的。dispatchDraw会遍历调用所有子元素的draw方法。这样draw事件就一层层传递了下来。它有个比较特殊的setWillNotDraw方法。
如果一个View不需要绘制任何内容,在我们设定这个标记为true后,系统就会对其进行相应优化。一般View没有启用这个标记位。但ViewGroup是默认启用的。
它对实际开发的意义在于:我们的自定义控件继承与ViewGroup并且不具备绘制功能时,可以开启这个标记位方便系统进行后续优化。
更多推荐
所有评论(0)