XDRush

从源码角度聊聊View的工作原理

1 Android窗口模型

理解View工作原理之前,有几个基本概念需要阐述清楚:

1.1 Window

Window是一个抽象类,代表的就是手机屏幕,其具体实现是PhoneWindow,Android中所有的视图都是通过Window来呈现的,无论是Activity、Dialog还是Toast,它们的视图实际上都是依附在Window上,因此Window实际上是View的直接管理者。

1.2 ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的工作原理同ViewRoot紧密相连.

DecorView是Activity的顶级View,包含标题栏和内容栏,内容栏是一定的存在的,而且其id为android.R.id.content,通过setContentView()将布局文件添加到内容栏中,
这篇文章有助于我们理解

1
2
3
4
5
// 获取内容栏
ViewGroup viewGroup = (ViewGroup) findViewById(android.R.id.content);
// 获取我们通过setContentView()所设置的View
View view = viewGroup.getChildAt(0);

Window,Activity,DecorView,ContentView等基本概念,通过图来说明

1.3 DecorView的创建过程

View离不开DecorView,了解DecorView的创建过程对于理解View的工作原理大有裨益。以一个简单例子来引入:

1
2
3
4
5
6
7
8
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity_layout);
}
}

再来追踪下Activity的setContentView()方法:

1
2
3
4
5
6
7
8
public class Activity extends ContextThemeWrapper {
...
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID); // 调用PhoneWindow的setContentView来创建DecorView对象
initWindowDecorActionBar();
}
}

接下来进入到PhoneWindow中看看DecorView是如何被创建的:

1
2
3
4
5
6
7
8
9
10
11
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor(); // 这里创建DecorView.
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews(); // 这里不是很明白为啥要移除所有的view??
}
...
}
}

进入到installDecor()中进一步分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(); // 创建DecorView对象mDecor.
...
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
}

进入generateDecor()方法中:

1
2
3
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}

方法很简单,就是new一个DecorView对象。但是此时创建的DecorView仅仅是一个空白的FrameLayout,接下来便是通过generateLayout()方法初始化DecorView的结构:

1
2
3
4
5
6
7
protected ViewGroup generateLayout(DecorView decor) {
...
View view = mLayoutInflater.inflate(layoutResource, null);
decor.addView(view, new ViewGroup.LayoutParams(FILL_PARENT, FILL_PARENT));
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
...
}

源码中这个方法很长,但其主要作用就是加载android.R.id.content这个布局文件,然后添加到DecorView中,并初始化mContentParent。以上便完成了DecorView结构的初始化工作。

接下来再回到setContentView()方法中:

1
2
3
4
5
6
7
8
public void setContentView(int layoutResID) {
...
if (hasFeature(FEATURE_CONTENT_TRANSITIIONS)) {
...
} else {
mLayoutInflator.inflate(layoutResID, mContentParent); // 这里最终将我们设置的布局文件通过inflate加入到mContentParent中.
}
}

以上过程只是完成了DecorView的创建和初始化工作,创建好了之后实际上DecovView仍旧处于INVISIBLE状态,还需要将DecorView添加到屏幕上,这时就需要用到WindowManager。分下Activity的启动过程,首先调用ActivityThread的handleResumeActivity,接着调用Activity.onResume()方法,接着调用Activity.makeVisible()方法:

1
2
3
4
5
6
7
8
void makeVisible() {
if (!mWindowAdded) {
WindowManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

以上过程便DecorView的整个创建以及初始化过程,这种见省略了不少其他的操作。那么,DecorView是如何同ViewRootImpl关联上的呢?

继续以上的分析:

1
wm.addView(mDecor, getWindow().getAttributes());

WindowManager的真实实现是WindowManagerImpl,因此实际上调用的也是WindowManagerImpl.addView()方法:

1
2
3
4
5
6
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 实际上调用的是WindowManagerGlobal.addView()方法
mGlobal.addView(view, params, mDisplay, mParentWindow);
}

接下来进入到WindowManagerGlobal.addView()方法中,这个方法比较复杂,这里作节选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display); // 创建View和与之对应的ViewRoot对象
view.setLayoutParams(wparams);
mViews.add(view); // 保存view到WindowManagerGlobal中
mRoots.add(root); // 保存ViewRoot到WindowManagerGlobal中
mParams.add(wparams); // 保存布局参数到WindowManagerGlobal中
...
}
...
root.setView(view, wparams, panelParentView);
}

在这个方法中,创建View对应的ViewRoot对象,ViewRoot控制着一个视图的结构,里面包含于WindowManager通信的Binder对象、View所在界面的ContextImpl、DecorView等信息。

接下来进入到ViewRootImpl.setView()方法中:

1
2
3
4
5
6
7
8
9
10
11
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
...
requestLayout(); // 首次执行layout,这里会触发onAttachToWindow()和创建Surface,需进一步追踪!!
...
}
}
}

接下来看一下requestLayout()方法:

1
2
3
4
5
6
7
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

再进入到scheduleTraversals()方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
...
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); // 这里重点就是mTraversalRunnable
...
}
}
// mTraversalRunnable定义
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
...
performTraversals(); // !!!!!!大BOSS终于来了!!!!!!
...
}
}

OKAY!!终于要开始进入到View的绘制流程了!

2 View的绘制

View的绘制绘制流程是从performTraversals()方法开始的,经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的位置,draw负责将View绘制在屏幕上。其大致流程如下图所示:

上图描述了View绘制的大致流程。下面将详细分析每一步的流程:
tool-bar

2.1 理解MeasureSpec

理解MeasureSpec对理解View的测量过程是必须的,字面意思就是“测量说明书”,MeasureSpec从始至终一直参与者View的测量过程。

(1) MeasureSpec

MeasureSpec是一个32为的int值,高2为代表SpecMode,意思就是测量模式,低30位代表SpecSize,是指在某种测量模式下的规格大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
private static final int UNSPECIFIED = 0 << MODE_SHIFT;
private static final int EXACTLY = 1 << MODE_SHIFT;
private static final int AT_MOST = 2 << MODE_SHFIT;
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通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为方便操作,其提供了打包和解包操作。

SpecMode有三类:
UNSPECIFIED
父容器不对View作任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

EXACTLY
父容器已经检测出View所需的精确尺寸大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent和具体的数值这两种模式。

AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现,它对应于LayoutParams中的wrap_content。

(2) MeasureSpec和LayoutParams的对应关系

正常情况下,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步确定View的宽/高。

DecorView的MeasureSpec创建过程
在RootViewImpl中的measureHierarchy方法中:

1
2
3
4
5
6
7
8
private boolean measureHierarchy(final View host, final WindwoManager.LayoutParams lp,
final Resource res, final int desiredWindowWidth, final int desiredWindowHeight) {
...
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}

其中,childWidthMeasureSpec/childHeightMeasureSpec分别为屏幕的尺寸,不难看出,对于顶级DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。

普通View的MeasureSpec创建过程
对普通的View而言,其measure过程由ViewGroup传递而来。
先来看看ViewGroup的measureChildWidhMargins()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void More ...measureChildWithMargins(View child, int parentWidthMeasureSpec,
int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 获取子元素的MeasureSpec
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);
// 对子元素进行measure过程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上述代码展示了对ViewGroup中的子元素进行measure过程,在调用子元素的measure方法之前,会先通过getChildMeasureSpec()方法获取子元素的MeasureSpec,同时不难发现,子元素MeasureSpec的创建同父容器的MeasureSpec和子元素本身的LayoutParams直接相关,此外还与子元素的margin及padding有关。getChildMeasureSpec()方法则更进一步说明了子元素MeasureSpec计算过程,具体有兴趣的可以自行了解下。

至此,获取MeasureSpec过程也就结束了,这是View绘制的第一步。

2.2 View的measure过程

measure过程需分2中情况来讨论:原始的View和ViewGroup。对于原始的View,通过measure方法就完成了自身的测量过程,对于ViewGroup,除了完成自己的测量之外,还要去递归遍历调用所有子元素的measure方法。

(1) View的measure过程

承接上文,在View的measure()方法中,会调用onMeasure()方法,来看看onMeasure()方法:

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

可以看出,onMeasure()方法相当简单,setMeasuredDimension()顾名思义,就是设置View的宽/高,如何设置?来看下getDefaultSize()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

简单来看,对绝大多数情况(AT_MOST和EXACTLY),getDefaultSize()返回的就是specSize,而这个specSize就是View的测量大小。但要注意,View最终的大小是在layout阶段确定的,但是几乎在所有情况下View的测量大小总是和最终的大小是相等的。UNSPECIFIED较少用到,追踪源码其实也挺简单,这里不作讨论。

(2) ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程之外,还会递归遍历所有的子元素。ViewGroup是一个抽象类,没有重写View的onMeasure()方法,但是提供了measureChildren()的方法:

1
2
3
4
5
6
7
8
9
10
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, widthSpec, heightMeasureSpec);
}
}
}

上述代码中,ViewGroup进行measure时,会对每一个子元素进行measure,再来看看measureChild()方法:

1
2
3
4
5
6
7
8
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);
}

如此便最终实现了ViewGroup的测量过程!!但,归根到底,ViewGroup并没有定义测量的具体过程,这主要是因为ViewGroup是一个抽象类,其测量过程中的onMeasure()方法需要各个子类去具体的实现,比如LinearLayout、RelativeLayout等各自定义了自己的onMeasure()方法。由于ViewGroup子类的不同的布局特性,导致它们的测量细节各不相同,因此ViewGroup并没有实现onMeasure()方法,而是交给具体的子类来实现。具体的可以参考LinearLayout的onMeasure()过程,这里不作细述。

(3) 一个问题

刚入门Android的时候,特别是在做Dolphin中的一些动画时候,经常要获取某个特定View个宽/高,当时傻啦吧唧的试图在Activity的onCreate()中获取(因为View的宽/高需要比较早知道),可是无论是view.getWidth()还是view.getMeasuredWidth(),返回的都是0!无赖在onResume()中获取,发现偶尔不为0,但大多数时候都为0!!不知道有没有和我一样SHA的。

实际上,在onCreate()、onStart()、onResume()中都无法正确的获取View的宽/高信息,这是因为View的measure过程和Activity的生命周期不是同步执行的!无法保证在Activity的这些方法调用时,View的测量过程已经完成了,那么如果一定要获取View的宽/高信息呢?该如何做?下面提供几种方法:

Activity/View#onWindowFoucusChanged()方法中获取
onWindowFoucusChanged()方法说明View已经初始化完毕,宽/高信息已经准备好了,在这里获取View的宽/高信息基本是正确的。
(1) View.post(runnable)方法

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate() {
super.onCreate();
...
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}

(1) ViewTreeObserver方法方法
使用ViewTreeObserver的回调方法也可以完成View的测量,比如在OnGlobalLayoutListener这个接口中做。

1
2
3
4
5
6
7
8
9
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
...
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});

2.3 View的Layout过程

Layout过程的作用是ViewGroup用来确定其子元素的位置,当ViewGroup位置被确定之后,它在onLayout()中会遍历所有子元素并调用其layout()方法,在layout()方法中onLayout()方法被调用,最终确定所有元素的位置。

也就是:layout()方法确定View本身的位置,而onLayout()方法确定所有子元素的位置。

先来看看View的layout()方法中的关键代码:

1
2
3
4
5
6
7
8
9
10
// 4个参数分别表示4个顶点
public void layout(int l, int t, int r, int b) {
...
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);
...
}
}

从上面代码可以看出,layout()方法的大致流程是:首先通过setFrame()方法来设置View的4个顶点位置,进入setFrame()方法不难看出,其实也就是设置mLeft, mRight, mTop, mBottom这四个值,View的4个顶点确定之后,View在父容器中的位置也就确定了;然后,调用onLayout()方法,这个方法的作用是父容器确定其子元素的位置,onLayout()在View中并无具体的实现,同onMeasure()方法类似,其具体实现依赖于具体的布局,因此View和ViewGroup均没有真正实现onLayout()方法,我们以LinearLayout中的onLayout()方法为例:

1
2
3
4
5
6
7
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutVertical(l, t, r, b);
}
}

继续看看layoutVertical()实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void layoutVertical(int left, int top, int right, int bottom) {
...
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();
...
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}

可以看出,此方法遍历所有子元素并调用setChildFrame()方法来为子元素指定对应的位置,setChildFrame()中调用了View的layout()方法,最终又回到了上面所提到的情况。

回到前文所提到的:绝大多数情况下,View的测量宽/高和最终的宽/高是相等的,在这里也可以得到证明:

1
2
3
4
5
6
7
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}

而在setFrame()方法中,

1
2
3
4
5
6
7
8
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 这里对顶点参数进行赋值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}

从上面的onLayout()方法中知道,

1
2
right = left + measuredWidth;
bottom = top + measuredHeight;

因此,View的默认实现中,View的测量宽/高和最终的宽/高是相等的。除了一下情况:

1
2
3
4
// 重写View的layout方法
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100. b + 100);
}

这种情况下,View的最终宽/高总是比测量宽/高大100px。另外一种情况是,View需要多次measure测能确定自己的测量宽/高,前几次的测量过程中,其测量出来的宽/高有可能和最终的宽/高不一致,但最终来讲,测量宽/高还是和最终的宽/高相同。

2.4 View的draw过程

View的draw()过程比较简单,它的作用是将View绘制在屏幕上,draw()过程分为以下几部:
(1) 绘制背景;
(2) 绘制自己;
(3) 绘制children;
(4) 绘制装饰。

来简要看下draw()方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void draw(Canvas canvas) {
...
// Step 1, draw the background, if needed.
if (!dirtyOpaque) {
drawableBackground(canvas);
}
// skip stdp 2 & 5 if possible (common case)
if (!verticalEdges && !horizontalEdge) {
// step 3, draw the content
if (!dirtyOpaque) {
onDraw(canvas);
}
// step 4, draw the children
dispatchDraw(canvas);
// step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
...
// we're done...
return;
}
}

View绘制过程的传递是通过dispatchDraw()完成的,dispatchDraw()会遍历调用所有子元素的draw()方法,如此实现draw事件的层层传递。

3 References

[1] Android源码:http://grepcode.com/search/?query=google+android&entity=project,本文所有代码基于Android 5.1.1_r1
[2] 《Android开发艺术探索》