
- View的三大绘制流程 ⭐⭐⭐⭐⭐
- 你知道View绘制前的准备流程吗?⭐
- 什么是MeasureSpec?⭐⭐⭐
- 测量模式有哪三种?⭐⭐
- 为什么有时候getMeasuredWidth获取值为0?⭐⭐
- 绘制的顺序是怎么样?⭐⭐
View绘制前的流程
View绘制流程就是Measure() -> Laytou() -> Draw()
Step1:初始化PhoneWindow和WindowManager
- Activity 是在 ActivityThread 的 performLaunchActivity 中进行创建的,并在attach()方法中创建了PhoneWindow,并初始化WindowManager。
Step2:初始化DecorView
- 系统执行完attach()方法后,会执行onCreate()方法,在onCreate()方法中执行setContentView()方法将布局xml文件解析为View并设置到DecorView里面的ContentViews控件。这里的源码也对应
参考文档1
的第2小节。
- 系统执行完attach()方法后,会执行onCreate()方法,在onCreate()方法中执行setContentView()方法将布局xml文件解析为View并设置到DecorView里面的ContentViews控件。这里的源码也对应
Step3:ViewRootImpl创建和关联DecorView
- 接着在handleResumeActivity()方法中,通过WindowManager的addView()方法将DecorView添加到WindoManager里面。通过源码分析,WindowManager的addView()方法最终通过WindowManagerGlobal的实例去addView(),在 WindowManagerGlobal()方法中,会创建一个ViewRootImpl,也就是最后会把DecorView传给了ViewRootImpl中的setView()方法。ViewRootImpl是DecorView的管理者,是WindowManager与DecorView之间的连接器。
Step4:建立 PhoneWindow 和 WindowManagerService 之间的连接
- WindowManagerService是所有Window窗口的管理者,负责Window的添加删除、事件派发等。每一个Activity都有一个PhoneWindow对象,操作PhoneWindow对象进行显示等操作,都需要和WindowManagerService进行交互。在上面第三步中的ViewRootImpl的setView()方法中,会执行setView()方法,会调用 requestLayout()方法,并且通过 WindowSession 的 addToDisplay()方法 与 WindowManagerService 进行交互。WindowManagerService 会为每一个 Window 关联一个 WindowStatus。到此,我们已经把DecorView加载到Window中了。
Step5:建立与SurfaceFlinger的链接
- SurfaceFlinger主要对图层数据进行合成,然后发送到屏幕渲染。在第4步中,WindowStatus会创建一个SurfaceSession,SurfaceSession 会在 Native 层构造一个 SurfaceComposerClient 对象,它是应用程序与 SurfaceFlinger 沟通的桥梁。
Step6:申请Surface
- View绘制会从ViewRoot的performTraversals(),按照Measure() -> Layout() -> Draw()经典流程完成View绘制。不过在此之前还会执行一个重要的函数relayoutWindow()。代码如下:
1 | //frameworks/base/core/java/android/view/ViewRootImpl.java |
调用[注释1]
的relayoutWindow()方法,通过 WindowSession与WindowManagerService交互,即把Java层的Surface和Native层的Surface关联在一起。
- Step7:正式进入View绘制
- 接下来就是正式绘制View的整体流程,即
[注释2-4]
三步走。绘制会根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每个ViewGroup会负责通知自己的子View进行绘制,而每个子View控件则负责绘制自己。大致的工作流程图如下:
- 接下来就是正式绘制View的整体流程,即
从上面的图,可以看到有onMeasure(),onLayout(),onDraw()这三个函数,是我们在实现自定义View最常接触的三个函数,接下来以这三个函数为思路进行讲解。
View绘制 - Measure(测量)
Measure源码流程
Measure翻译过来即是“测量”的意思,在此测量的是每个控件的宽和高。在代码层面,则是给每个View的mMeasuredWidth和mMeasuredHeight变量进行赋值。在测量时遵循:
- 如果是ViewGroup,则遍历测量子View的宽高,再根据子View宽高算出自身的宽高;
- 如果是子View,则直接测量出自身宽高;
现在从[注释2]
的performMeasure()方法开始:
1 | //frameworks/base/core/java/android/view/ViewRootImpl.java |
逻辑很清晰,可发现实际起作用的是[注释5]
mView.measure()方法,
1 | //frameworks/base/core/java/android/view/View.java |
measure()方法使用final修饰,代表不可重写。在measure()方法中会进行一系列逻辑处理后,调用[注释6]
的onMeasure()方法,真正的测量都在onMeasure()方法中实现。
1 | //frameworks/base/core/java/android/view/View.java |
可以看到onMeasure()方法使用protected修饰,代表我们可以重写该方法。因此如果需要实现自己的测量逻辑,只能通过子View重写onMeasure()方法,而不能重写measure()方法。onMeasure()最后调用[注释7]
setMeasuredDimension()设置View的宽高信息,完成View的测量操作。
看看getDefaultSize()的源码:
1 | public static int getDefaultSize(int size, int measureSpec) { |
这是系统设置默认的尺寸,在[注释8]
可以看到如果specMode是AT_MOST或者EXACTLY,则返回的就是specSize。至于 UNSPECIFIED 的情况,则会返回一个建议的最小值,这个值和子元素设置的最小值它的背景大小有关(这一段话可先看看2.2小节再回来继续看)。
从一开始执行的performMeasure()到最后设置宽高的setMeasuredDimension()方法,流程都比较清晰。并且可以发现有两个贯穿整个流程的变量,widthMeasureSpec和heightMeasureSpec,理解这两个变量才是关键。
什么是MeasureSpec
MeasureSpec是一个32位的int型数据,由两部分组成,SpecMode(测量模式,高2位) + SpecSize(测量尺寸,低30位)。将这两者打包为一个int数据可以起到节省内存的作用。有打包当然也有解包的方法:
1 | //获取测量模式 |
名词解析:控件的
布局参数LayoutParams
是指控件设定为match_parent或者wrap_content或具体数值之中的一种。
测量模式
- EXACTLY:确定大小,父View希望子View的大小是确定的。对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值;
- AT_MOST :最大大小,父View希望子View的大小最多是specSize指定的值。对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
- UNSPECIFIED :不确定大小,父View完全依据子View的设计值来决定。系统不对View进行任何限制,要多大给多大,一般用于系统内部。
具体详见2.2.2小节的图。
MeasureSpec如何确定
- DecorView:通过屏幕大小和自身布局参数LayoutParams,只要将自身大小和屏幕大小相比,设置一个不超过屏幕大小的宽高和对应测量模式即可;
- ViewGroup和View:需要通过父布局的MeasureSpec和自身的布局参数LayoutParams确定,具体如下:
ViewGroup的测量
上面说过ViewGroup需要测量其包含的子View的宽高后,根据子View宽高算出自身的宽高。所以在ViewGroup中定义了measureChildren(), measureChild(), measureChildWithMargins()方法来对子视图进行测量,measureChildren()内部实质只是循环调用measureChild()。
1 | //frameworks/base/core/java/android/view/ViewGroup.java |
以上代码注释已经写的比较清楚了,关键看看getChildMeasureSpec()方法:
1 | //frameworks/base/core/java/android/view/ViewGroup.java |
代码逻辑也非常清楚,首先根据父控件的specMode模式,结合子控件的布局参数LayoutParams,最后得到子控件的MeasureSpec属性。有一点需要注意,如果父控件的MeasureSpec是AT_MOST,对应[注释a]
,无论子控件的布局参数是WRAP_CONTENT还是MATCH_PARENT,最终获得的子控件的specMode模式都是AT_MOST,即[注释b-c]
。因此,一般的解决方法是当布局参数是WRAP_CONTENT时,在onMeasure()方法里手动指定一下默认的宽和高:
1 |
|
Measure总结
- measure过程主要就是从顶层父View向子View递归调用view.measure方法进行测量(measure()中又回调onMeasure()方法)的过程;
- 如果是ViewGroup则需执行要measure()并重写onMeasure()方法,在该方法中定义自己的测量方式,接着调用maesureChildren()方法遍历测量子View的宽高,最终根据子View宽高确定自己的宽高;
- ViewGroup类提供了measureChild(),measureChildren()和measureChildWithMargins()方法,简化了父子View的尺寸计算;
- 如果是子View则调用measure() -》 onMeasure()方法完成自身的测量即可;
- View的measure()方法是final修饰的,不能重写,只能重写onMeasure()方法完成自己的测量,且重写时不建议把宽高设置为死值;
- 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
流程图(摘抄于参考文档2)如下:
View绘制 - Layout(布局)
Layout源码流程
将所有的View的宽高都计算好之后,就开始将所有的View进行布局了,即在Window摆放好所有View的位置,View的布局主要是通过确定上下左右四个关键点来确定其位置。值得一说的是,测量时,先测量子View的宽高,再测量父View的宽高。但是在布局时顺序则相反,是父View先确定自身的布局,再确认子View的布局。还是从[注释3]
的performLayout()方法开始看:
1 | //frameworks/base/core/java/android/view/ViewRootImpl.java |
[注释9]
的host其实是View类,因此会调用View.java里面的layout()方法。
1 | //frameworks/base/core/java/android/view/View.java |
上面代码有几个函数需要了解:
- setFrame():确定View自身位置;
- setOpticalFrame():也是确定View自身位置,其内部也是通过调用setFrame()来实现;
- onLayout():确认该View里面的子View在父容器的位置,用protected修饰,在View.java文件里的onLayout()只是个空函数,需要子类进行重写。
LinearLayout的onLayout()
如果当前的View是一个子View,则不需要重写onLayout()。但如果是一个ViewGroup,则先执行layout()方法 –〉setFrame()方法确定自己的位置,再通过重写onLayout(),其中的关键是循环调用child.layout()方法来确定子View在当前父容器的位置。我们看看LinearLayout的onLayout():
1 | //frameworks/base/core/java/android/widget/LinearLayout.java |
如果LinearLayout是垂直布局,则调用layoutVertical()。接着计算子View的个数,循环调用[注释12]
的setChildFrame()方法来确定子View在父容器的位置。从[注释13]
可以知道setChildFrame()方法其实就是调用子View的layout()方法。而子View会继续调用setFrame()方法确定自己的位置,在执行onLayout()方法,上面说过在View.java文件里的onLayout()只是个空函数,所以此时不会有具体实现。
Layout流程图
看看Layout的流程图加深理解(抄录于参考文档2):
View绘制 - Draw(绘制)
Draw源码流程
每个View的宽高和位置都确定好后,就开始最后的绘制了,从[注释4]
performDraw()开始看:
1 | //frameworks/base/core/java/android/view/ViewRootImpl.java |
Draw的步骤
通过源码可以追溯到,最终执行到了View.java里面的draw()方法。
1 | //frameworks/base/core/java/android/view/View.java |
以上的源码里的官方注释,draw()方法有以下步骤:
- 绘制View的背景;
- 如果有必要的话,保存画布的图层以准备fading;
- 绘制View的内容,即执行关键函数
onDraw()
; - 绘制子View;
- 如果有必要的话,绘制View的fading边缘并恢复图层;
- 绘制View的装饰(比如滚动条等等)
- 绘制默认焦点高亮
无论是View还是ViewGroup,绘制的流程都是如此,还有两点需要了解:
- 在ViewGroup中,实现了dispatchDraw()方法,而子View是不需要实现该方法的;
- 自定义View时,一般需要重写onDraw()方法,以绘制自己想要的样式,自定义View可见本系列《自定义View》一文。
Draw的流程图
看看Draw()的流程图加深理解(抄录于参考文档2):
为什么view.post()能保证获取到view的宽高
1 | public boolean post(Runnable action) { |
- 当AttachInfo为null时,则将任务加入当前View的等待队列中
- 将传入的任务封装成HandlerAction对象
- 创建一个默认长度为4的 HandlerAction数组,用于保存通过post()添加的任务
- 注此时只是保存了通过post()添加的任务,并没执行。
- 当AttachInfo不为null时,直接调用其内部Handler的post()
- 遍历前面过程保存了通过post()添加的任务的数组,将每个任务发送到handler中等待执行。
attachInfo的赋值过程 -> dispatchAttachedToWindow()调用时机是在 View 绘制流程的开始阶段(),即 ViewRootImpl.performTraversals()
因此
- 通过View.post()添加的任务是在View绘制任务里 - 开始绘制阶段时添加到消息队列的尾部的;
- 所以,View.post() 添加的任务的执行是在View绘制任务后才执行,即在View绘制流程结束之后执行
- 即View.post() 添加的任务能够保证在所有 View绘制流程结束之后才被执行,所以 执行View.post() 添加的任务时可以正确获取到 View 的宽高。
总结
- View的绘制有三大经典步骤:测量-布局-绘制,如果需要自定义View,可能需要重写onMeasure()方法,onLayout()方法,onDraw()方法;
- 测量时,先测量子View再根据子View大小,计算出父View的大小;
- 布局时,先布局好父View的位置,再布局子View的位置;
- 绘制时,先绘制背景,再绘制父View,最后绘制子View;
绘制优化
主要优化方向是:
- 降低View.onDraw()的复杂度
- 避免过度绘制(Overdraw)
降低View.onDraw()的复杂度
onDraw()中不要创建新的局部对象
避免onDraw()执行大量 & 耗时操作
避免过度绘制(Overdraw)
过度绘制会导致屏幕显示的色块不同(背景白色+背景上绘制文字)
- 移除默认的 Window 背景
- 一般情况下,该默认的 Window 背景基本用不上:因背景都自定义设置。若不移除,则导致所有界面都多 1 次绘制
- 移除控件中不必要的背景
- 对于1个ViewPager + 多个 Fragment 组成的首页界面,若每个Fragment 都设有背景色,即 ViewPager 则无必要设置,可移除
- 减少布局文件的层级(嵌套)
- 使用布局标签
<merge>
& 合适选择布局类型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// 使用说明:
// 1. <merge>作为被引用布局A的根标签
// 2. 当其他布局通过<include>标签引用布局A时,布局A中的<merge>标签内容(根节点)会被去掉,在<include>里存放的是布局A中的<merge>标签内容(根节点)的子标签(即子节点),以此减少布局文件的层次
/**
* 实例说明:在上述例子,在布局B中 通过<include>标签引用布局C
* 此时:布局层级为 = RelativeLayout ->> Button
* —>> RelativeLayout ->> Button
* ->> TextView
* 现在使用<merge>优化:将 被引用布局C根标签 的RelativeLayout 改为 <merge>
* 在引用布局C时,布局C中的<merge>标签内容(根节点)会被去掉,在<include>里存放的是布局C中的<merge>标签内容(根节点)的子标签(即子节点)
* 即 <include>里存放的是:<Button>、<TextView>
* 此时布局层级为 = RelativeLayout ->> Button
* ->> Button
* ->> TextView
* 即 已去掉之前无意义、多余的<RelativeLayout>
*/
// 被引用的公共部分:布局C = layout_c.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_10"/>
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_10"/>
</merge>
// 布局B:layout_b.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/Button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/dp_10" />
<include layout="@layout/layout_c.xml" />
</RelativeLayout>
- 使用布局标签
- 自定义控件View优化
- 使用 clipRect(): 给 Canvas 设置一个裁剪区域,只有在该区域内才会被绘制,区域之外的都不绘制
- quickReject(): 若判断与矩形相交,则可跳过相交的区域,从而减少过度绘制