Android 之 View 绘制流程
Android 之 View 绘制流程
记忆点:
Android 界面是怎么显示的? -> setContentView()
涉及到 LayoutInflater -> View 的绘制流程
setContentView 与 LayoutInflater
getWindow().setContentView()
Activity 中有一个成员为 Window, 其实例化对象为 PhoneWindow, PhoneWindow 为抽象类 Window 的实现类:
- Window 是一个抽象类,提供了绘制窗口的一组通用 API
- PhoneWindow 是 Window 的具体继承实现类,而且该类内部包含了一个 DecorView 对象,该 DecorView 对象是所有应用窗口(Activity 界面)的根 View
- DecorView 是 PhoneWindow 的内部类,是 FrameLayout 的子类,是所有应用窗口的根 View
窗口 PhoneWindow 的setContentView()
Activity 中三个重载的setContentView()
:setContentView(int layoutResID)
、setContentView(View view)
、setContentView(View view, ViewGroup.LayoutParams params)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public voids setContentView(int layoutResID){
if(nContentParent == null){
// 是否是第一次调用
installDecor();
}else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 默认为 false
mContentParent.removeAllViews();
}
if(hasFeature(FEATURE_CONTENT_TRANSITIONS)){
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
transitionTo(newScene);
}else {
// mLayoutInflater 是在 PhoneWindow 的构造函数中得到实力对象的 LayoutInflater.from(context);
mLayoutInflater.inflate(layoutResID, mContentParent);
}
final Callback cb = getCallback();
if(cb != null && !isDestroy()){
cb.onContentChanged();
}
}
1 | public void setContentView(View view, ViewGroup.LayoutParams params){ |
setContentView(View view)
内部也是调用setContentView(View, ViewGroup.LayoutParams params)
,只是 LayoutParams 设置为了 MATCH_PARENT 而已。在程序中可多次调用setContentView()
,因为会removeAllViews()
;流程可概括为:
- 创建一个 DecorView 的对象 mDecor,该对象作为整个应用窗口的根视图。
- 根据 Feature 等 style theme 创建不同的窗口修饰布局文件,并通过
findViewById()
获取 Activity 布局文件存放的地方(窗口修饰布局文件 id 为 content 的 FrameLayout 内)。
当启动 Activity 掉运完 ActivityThread 的main()
方法之后,接着调用 ActivityThread 类的performLaunchActivity()
来创建需要启动的 Activity 组件,在创建 Activity 组件的过程中,还会为该 Activity 组件创建窗口对象和视图对象,接着 Activity 组件创建完成后,通过调用 ActivityThread 的hadnleResumeActivity()
将它激活,在方法中r.activity.makeVisible()
显示我们上面setContentView()
创建的 mDecor 视图族。
LayoutInflater 机制原理
获取 LayoutInflater 实例的两种写法:1
2LayoutInflater lif = LayoutInflater.from(context);//其实就是对下面方式的封装
LayoutInflater lif = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layoutInflater.inflate()
1
2
3public View inflate (int resource, ViewGroup root){
return inflate(resource, root, root != null);
}
layoutInflater.inflate(resource, root)
->layoutInflater.inflate(resource, root, true/false)
1
2
3
4
5
6
7
8
9
10
11public View inflate(int resource, ViewGroup root, boolean attachToRoot){
final Resources res = getContext().getResources();
...
//Android 默认实现为 Pull 解析的实现类 XmlPullParser
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
layoutInflater.inflate(resource, root)
->layoutInflater.inflate(resource, root, attachToRoot)
->inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot){
synchronized(mContructorArgs){
Trace.tarceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mContructorArgs[0];
mContructorArgs[0] = mContext;
View result = root;
try {
int type;
// 找根节点
while((type = parser.next) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT){
// empty
}
// 如果
if(type != XmlPullParser.START_TAG){
throw new InflateException(parser.getPositionDescription() + ": No start tag found!")
}
final String name = parser.getname();
if(TAG_MERGE.equals(name)){
if(root == null || !attachToRoot){
throw new InflateException("merge can be used only with a valid ViewGroup root and attach = true");
}
rInflate(parser, root, attrs);
}else {
// 方法内部又会去调用 createView()
// 然后使用反射的方式创建出 View 的实例并返回
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
if(root != null){
params = root.generateLayoutParams(attrs);
if(!attachToRoot){
temp.setLayoutParams(params)
}
}
rInflate(parser, temp, attrs);
if(root != null && attachToRoot){
root.addView(temp, params);
}
if(root == null || !attachToRoot){
result = temp;
}
}
}catch(XmlPullParserException e){
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
}catch(IOException e){
InflateException ex = new InflateException(parser.getPositionDescription() + ": " + e.getMessage());
ex.initCause(e);
throw ex;
}
return result
}
}
LayoutInflater 使用的是 Android 提供的 pull 解析方式来解析 xml 布局文件的。以上只是创建了一个根布局实例而已,接下来会调用rInflate()
方法来循环遍历这个根布局下的子元素。其中获取 View 的方法是createViewFromTag()
,通过反射的方式。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
26
27private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs){
final int depth = parser.getDepth();
int type;
while(()(type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT){
if(type != XmlPullParser.START_TAG){
continue;
}
final String name = parser.getName();
if(TAG_REQUEST_FOCUS.equals(name)){
parseRequestFocus(parser, parent);
}else if(TAG_INCLUDE.equals(name)){
if(parser.getDepth() == 0){
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
}else if(TAG_MERGE.equals(name)){
throw new InflateException("<merge /> must be the root element");
}else{
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGruop.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs);
viewGroup.addView(view, params);
}
}
parent.onFinishInflate();
}
由上可以看出,布局嵌套过深不仅效率低还可能引起调运栈溢出。同时在解析 tag 时也有一些特殊处理(include\merge)。布局优化可以通过 hierarchyviewer 来查看,通过 Lint 也可以自动检查出来一些。关于布局优化
setContentView()
整个过程主要是如何把 Activity 的布局文件或者 Java 的 View 添加至窗口:
- 创建一个 DecorView 的对象 mDecor,该对象作为整个应用窗口的根视图;
- 依据 Feature 等 style theme 创建不同的窗口修饰布局文件,并通过 findViewById 获取 Activity 布局文件该存放的地方(窗口修饰布局文件中 id 为 content 的 FrameLayout);
- 将 Activity 的布局文件添加到 id 为 content 的 FrameLayout 内;
- 当
setContentView()
设置显示为 OK 以后会回调 Activity 的onContentChanged()
。Activity 的各种 View 的findViewById()
方法等都可以放到该方法中,系统会帮忙回调。
View 绘制流程与源码分析:
每一个 View 绘制都经历三个最主要的过程:measure -> layout -> draw
整个 View 树的绘图流程是在 ViewRootImpl 类的performTraversals()
开始的。该函数功能主要是根据之前设置的状态,判断是否重新计算视图的大小measure
,是否重新安放视图位置layout
,以及是否重绘draw
。1
2
3
4
5
6
7
8
9
10
11
12private void performTraversals(){
...
// lp.width 和 lp.height 在创建 ViewGroup 实例的时候就被赋值了,都等于 MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
...
mView.draw(canvas);
}
1 | /** |
其中 mView 就是 View 对象。以下就是整个流程的大致流程图:
View 绘制第一步measure
View 的measure()
1
2
3
4
5public final void measure(int widthMeasureSpec, int heightMeasureSpec){
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
为整个 View 树设置实际的阿晓,然后设置实际的高和宽,每个 View 控件的实际宽高都是由父视图和自身决定的。measure()
不可被重写,实际的测量是在onMeasure()
中进行。
关于 widthMeasureSpec/heightMeasureSpec,这两个参数是从父 View 传递过来,代表了父 View 的规格。
MeasureSpec: 32位,也就是 specMode + specSize,高2位表示 MODE,低30位表示 size (也就是父 View 的大小)
MeasureSpec.EXACTLY:表示父视图希望子视图的大小应该是由 specSize 的值来决定的,系统会默认按照这个规则来设置子视图的大小,开发人员也可按照自己的意愿设置成任意大小。
MeasureSpec.AT_MOST:表示子视图最多只能是 specSize 中指定的大小,开发人员应该尽可能小的去设置这个视图,并且保证不会超过 specSize。系统会默认按照这个规则来设置子视图大小,开发人员也可以按照自己的意愿设置成任意大小。
MeasureSpec.UNSPECIFIED:表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
measure()
->onMeasure()
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ |
onMeasure()
是真正去测量并设置 View 大小的地方,默认会调用getDefaultSize()
方法来获取视图的大小。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public 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;
}
这里传入的 measureSpec 是一直measure()
中传递过来的。系统这时默认行为是如果传递过来的 specMode 是 AT_MOST 或是 EXACTLY 就返回 specSize。之后通过setMeasuredDimension()
来设定测量出的大小,这样一次measure
过程就结束了。而一个布局中一般都会包含多个子视图,每个视图都需要一次measure
的过程。在 ViewGroup 中定义了measureChild()
、measureChildren()
、measureChildWithMargins()
方法来对子视图进行测量。measureChildren()
内部实质只是循环调用了measureChild()
,measureChild()
和measureChildWithMargins()
的区别只是是否把 margin 和 padding 也作为子视图的大小。1
2
3
4
5
6
7
8
9
10protected 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) != GONE){
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
1 | protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec){ |
getChildMeasureSpec()
去计算子视图的 MeasureSpec,计算的依据是布局文件中定义的 MATCH_PARENT、WRAP_CONTENT 等值。然后再调用子视图的measure()
,并把计算出的 MeasureSpec 传递进去,之后的流程与前面介绍的相同。
measure()
原理总结
从顶层父 View 向子 View 递归调用view.measure()
(measure()
中又回调onMeasure()
)
- MeasureSpec:EXACTLY、AT_MOST、UNSPECIFIED
- View 的
measure()
是 final 的,不允许重写,View 子类只能重写onMeasure()
来完成自己的测量逻辑 - 顶层 DecorView 测量时的 MeasureSpec 是由 ViewRootImpl 中的
getRootMeasureSpec()
方法确定的(LayoutParams 宽高参数均为 MATCH_PARENT,specMode 是 EXACTLY,specSize 为物理屏幕的大小)。 - 使用 View 的
getMeasuredWidth()
和getMeasuredHeight()
方法来获取 View 测量的宽高,必须保证这两个方法在onMeasure()
流程之后被调用才能返回有效值。
View 绘制第二步layout
当 ViewRootImpl 的performTraversals
中的measure()
执行完成以后会接着执行mView.layout()
1 | private void performTraversal(){ |
layout()
接收的四个参数,分别代表着左、上、右、下的坐标,这个坐标是相对于当前视图的父视图而言的
整个 View 树的layout()
递归流程:
layout 也是递归结构,ViewGroup 的layout()
实际还是调用的 View 父类的layout()
方法
1 | public void layout(int l, int t, int r, int b){ |
1 |
|
ViewGroup 中的onLayout()
竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法,在内部按照自己的规则对子视图进行布局。以 LinearLayout 为例子:
1 | public class LinearLayout extends ViewGroup{ |
1 | void layoutVertical(int left, int top, int right, int bottom){ |
以上可以看到,一般情况下layout()
过程会参考measure()
过程中计算得到的 mMeasuredWidth 和 mMeasuredHeight 来安排子 View 在父 View 中显示的位置,但这不是必须的,measure()
过程得到的结果可能完全没有实际用处。
layout()
原理总结
- View 的
layout()
方法可被重写,但是 ViewGroup 的layout()
为 final,ViewGroup 的onLayout()
为 abstract 的,子类必须重写实现自己的布局逻辑 measure()
操作后得到的是每个 View 经过测量过的 measuredWidth 和 measuredHeight,layout()
操作之后得到的是对每个 View 进行位置分配后的 mLeft、mTop、mRight、mBottom,这些都是相对父 View 来说的- 凡是 layout_XXX 的布局属性基本都是针对的是包含子 View 的 ViewGroup 的。为一个没有父容器设置相关的 layout_XXX 属性是没有任何意义的
- 使用 View 的
getWith()
和getHeight()
方法来获取 View 测量的宽高,必须保证这两个方法在onLayout()
流程之后被调用才能返回有效值
View 绘制第三步draw
draw()
过程也是在 ViewRootImpl 的performTraversals()
内部调用的,其调用顺序在measure()
和layout()
之后。
1 | private void performTraversals(){ |
View 递归 draw 流程:
1 | public void draw(Canvas canvas){ |
第一步,对 View 的背景进行绘制
1 | private void drawBackground(Canvas canvas){ |
第三步,对 View 的内容进行绘制(第二、第五步可以跳过)
1 | protected void onDraw(Canvas canvas){} |
空方法。这是因为每个 View 的内容部分是各不相同,需要具体子类实现具体逻辑
第四步,对 View 的所有子 View 进行绘制
1 | protected void dispatchDraw(Canvas canvas){} |
也是一个空方法。如果 View 包含子 View 需要重写它,看看 ViewGroup 的dispatchDraw()
1 |
|
ViewGroup 确实重写了 View 的dispatchDraw()
,该方法内部会遍历每个子 View,然后调用drawChild()
1 | protected boolean drawChild(Canvas canvas, View child, long drawingTime){ |
drawChild()
调用了子 View 的draw()
。ViewGroup 类已经为我们重写了dispatchDraw()
的功能实现,一般不需要重写该方法,但可以重写父类函数实现具体的功能
第六步,对 View 的滚动条进行绘制
onDrawScrollBars(canvas);
draw()
原理总结:
- 如果该 View 是一个 ViewGroup,则需递归绘制其包含的所有子 View
- View 默认不会绘制任何内容,真正的绘制都需要在子类中实现
- View 的绘制是借助
onDraw()
方法传入 Canvas 类进行的 - 默认情况下的子 View 的 ViewGroup.drawChld 的绘制顺序和子 View 被添加的顺序一致,但是你可以重写
ViewGroup.getChildDrawingOrder()
方法提供不同的顺序。
视图状态和视图重绘
视图状态
常见几个:enabled(是否可用,不可用无法响应onTouch
事件)、focused、window_focused、selected、pressed…
视图重绘
调用setVisibility()
、setEnabled()
、setSelected()
等方法时都会导致视图重绘,而如果想要手动强制让视图进行重绘,可用通过调用invalidate()
来实现。其实上述方法内部也是调用此方法实现重绘。各种重载的invalidate()
最终调用的都是invalidateInternal()
1 | void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean full Invalidate){ |
这是一个向上级父 View 回溯的过程,每一层的父 View 都将自己的显示区域与传入的刷新的 Rect 做交集。看看 ViewGroup 的 invalidateChild 方法:
1 | public final void invalidateChild(View child, final Rect dirty){ |
以上过程最后传递到 ViewRootImpl 的invalidateChildInParent()
方法结束
1 |
|
postInvalidate()
实际就是子线程通过 ViewRootImpl 类的dispatchInvalidateDelayed()
,通过 Handler 发送 MSG_INVALIDATE 消息,ViewRootImpl 中的 Handler 收到消息就又在 UI 线程中调用了 View 的invalidate()
方法。
其他
setContentView()
的addView()
会通过invalidate(true)
触发 ViewRootImpl 类的`performTraversals()
requestLayout()
层层上传到 ViewRootImpl 后触发 ViewRootImpl 中的requestLayout()
中的scheduleTraversals()
。requestLayout()
会调用measure()
过程和layout()
过程,但不会调用draw()
,也不会重绘任何子 View包括调用者自身。
参考引用
Android LayoutInflater原理分析,带你一步步深入了解View(一)