13.View绘制流程分析
目录介绍
- 01.addView的流程分析
- 1.1 wm.addView()流程
- 02.requestLayout绘制
- 2.1 源码流程分析
- 2.2 View绘制流程简析
- 03.performMeasure测量
- 3.1 performMeasure源码
- 3.2 measure设计思路
- 3.3 measure测量流程
- 04.performLayout布局
- 4.1 performLayout源码
- 4.2 layout设计思路
- 4.3 layout布局流程
- 05.performDraw绘制
- 5.1 performDraw源码
- 5.2 draw设计思路
- 5.3 draw绘制流程
- 06.View绘制流程总结下
- 6.1 Activity布局绘制
- 07.View刷新操作
- 7.1 View如何显示在屏幕
- 7.2 requestLayout刷新
- 7.3 invalidate刷新
- 7.4 postInvalidate刷新
00.问题答疑思考
- requestLayout、invalidate与postInvalidate作用与区别,在requestLayout这个方法里面做了什么?
- requestLayout,onLayout,onDraw,DrawChild区别与联系?drawChild()是做什么用的?
- View是如何绘制到屏幕上的?View的刷新机制是什么,有哪些重要的方法?
- Canvas.save()跟Canvas.restore()的调用时机?Canvas的底层机制,绘制框架,硬件加速是什么原理,canvas lock的缓冲区是怎么回事
- View绘制流程,当一个TextView的实例调用setText()方法后执行了什么?请说一下原理……
- 测量:如何理解View中的测量?子控件的测量依赖父布局约束吗?如何理解测量过程中的"递"和"归"的设计思想?
- 测量:如何理解测量中布局MeasureSpec的设计?mode三种布局模式分别如何理解?mode和size的组成如何理解?
- 测量:单个控件测量流程是怎么样的?测量策略是如何影响测量结果的?如何标记测量完成?谈一下设计思想?
- 测量:完整的View树测量流程的设计思路是什么样的?遍历测量孩子的大小如何处理margin和padding逻辑?
- 布局:getWidth()方法和getMeasureWidth()区别呢?布局的设计思路是什么?
- 布局:单个View的布局流程是怎么样的?如何理解布局中相对位置和绝对位置?什么情况下需要对控件重新布局?
- 布局:以LinearLayout为例,完整布局流程是怎么样的。如果其中的一个孩子View修改了top高度,其布局流程会发生什么变化?
- 绘制:在View中draw绘制思路是如何设计的?ViewGroup中dispatchDraw分发绘制设计思路是怎样的?
- 绘制:说一下绘制中surface起到什么作用?draw里面绘制了那些东西,简要介绍一下?
01.addView的流程分析
1.1 wm.addView()流程
- 通过WindowManager添加View分析
wm.addView(),根据WindowManagerImpl --> WindowManager --> ViewManager,最终可知vm是WindowManagerImpl的实例,具体看WindowManagerImpl的addView方法 WindowManagerImpl#addView(),在这个类中可以看到调用了mGlobal.addView(),而mGlobal是WindowManagerGlobal的对象。 WindowManagerGlobal#addView(),这里面逻辑很核心,创建ViewRootImpl对象,这个是布局渲染的核心类 WindowManagerGlobal#addView#root.setView(),实现了root与ViewRootImpl的关联 ViewRootImpl#setView()#requestLayout(),在ViewRootImpl的setView方法中,调用requestLayout执行重绘的请求
- 从这里可以知道几个核心的关键点
- 在global#addView()的源码中,创建ViewRootImpl对象root,然后将root和view绑定起来
02.requestLayout绘制
2.1 源码流程分析
- requestLayout是执行View绘制入口
ViewRootImpl#requestLayout#checkThread(),这个方法是检查当前线程的方法,若当前线程非UI线程,则抛出非UI线程更新UI的错误 ViewRootImpl#scheduleTraversals(),这里主要是入口 ViewRootImpl#scheduleTraversals()#mChoreographer.postCallback(mTraversalRunnable),调用一个异步消息,用于执行mTraversalRunnable的run方法 ViewRootImpl#TraversalRunnable#run(),在TraversalRunnable类的run方法中调用了doTraversal方法 ViewRootImpl#doTraversal(),这里主要是看performTraversals方法 ViewRootImpl#performTraversals(),整个View的绘制起始方法,从这个方法开始我们的View经过大小测量,位置测量,界面绘制三个逻辑操作
ViewRootImpl#performTraversals
方法中核心代码如下所示private void performTraversals() { performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); performLayout(lp, mWidth, mHeight); performDraw(); }
image
2.2 View绘制流程简析
- 接下来看一下View的测量,位置,绘制三个流程
image
ViewRootImpl#performTraversals()#performMeasure,执行View组件的onMeasure方法,主要用于测量View ViewRootImpl#performMeasure(),开始执行测量的入口 View#measure(),这个时候开始调用View类中的测量 ViewRootImpl#performTraversals()#performLayout,执行View组件的onLayout方法,主要用于布局View ViewRootImpl#performLayout(),开始执行布局的入口 View#layout(),这个时候调用调用View类中的布局 ViewRootImpl#performTraversals()#performDraw,执行View组件的onDraw方法,主要用于绘制View ViewRootImpl#performDraw(),开始执行绘制的入口
- View的绘制流程主要分为三步:
- onMeasure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成绘制工作。
- onLayout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。
- onDraw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。
- onMeasure()方法:
- 单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
- Measure完成后可以通过getMeasureWidth和getMeasureHeight方法获取到view的测量后的宽高,在几乎所有的情况下都会等于最终view的宽高
- onMeasure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
- onLayout()方法:
- 单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
- layout 过程决定了View的四个顶点的坐标和实际的View的宽高,完成以后可以通过getTop,getBottom,getLeft,getRight来获取View的四个顶点位置,并通过getWidth,getHeight获取View的最终宽高
- onDraw()方法:
- 无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法
- draw过程则决定了View的显示,完成draw后view会显示在屏幕上
- 绘制背景(background.draw(Canvas))
- 绘制自己 protected void onDraw(Canvas canvas) onDraw绘制自己,新建一个paint 在canvas上绘制自己的图形
- 绘制children (dispatchDraw)dispatchDraw会遍历调用所有子元素的draw方法
- 绘制装饰(onDrawScrollBars)
03.performMeasure测量
3.1 performMeasure源码
- 从ViewRootImpl类中分析performMeasure测量,这里是测量的入口
ViewRootImpl#performMeasure(),在performMeasure方法中我们又调用了mView的measure方法。这里的mView就是一开始的Activity的mDector根组件,这里的measure方法就是调用的mDector组件的measure方法 View#measure(),在View的measure方法中,又调用了onMeasure方法。由于我们的mDector对象是一个FrameLayout,所以这里的onMeasure执行的是FrameLayout的onMeasure方法 FrameLayout#onMeasure(),这里调用了一个循环逻辑,获取该View的所有子View,并执行所有子View的measure方法,这样又回到View的measure方法。 - 这样经过一系列的循环遍历过程,如果是ViewGroup就会调用其ViewGroup的onMeasure方法,若果是View组件就会调用View的onMeasure方法 View#onMeasure(),调用了setMeasuredDimension方法设置测量的结果 View#setMeasuredDimension(),在这个方法中调用了setMeasuredDimensionRaw方法,把View组件即其子View的大小测量出来了,并且保存在了成员变量mMeasuredWith和mMeasuredHeight中
- 需要注意测量最后一定要调用setMeasuredDimension()
- Android提供了setMeasureDimension()函数,将测量结果作为参数并调用该函数,便可以视为View完成了自身的测量。该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高。
3.2 measure设计思路
- View的测量目标便是测量控件的宽高值,View的设计者通过代码编织了一整套复杂的逻辑:
- 1、对于子View而言,其本身宽高直接受限于父View的布局要求,举例来说,父View被限制宽度为40px,子View的最大宽度同样也需受限于这个数值。因此,在测量子View之时,子View必须已知父View的布局要求,这个 布局要求, Android中通过使用 MeasureSpec 类来进行描述。
- 2.1、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。Android中View的测量流程中使用了非常经典的 递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件...这种通过遍历自顶向下传递数据的方式我们称为 测量过程中的“递”流程。
- 2.2、当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,这种从底部向上传递各自的测量结果,最终完成最顶层父控件的测量方式我们称为测量过程中的“归”流程,至此界面整个View树测量完毕。
- 需要注意的是,子控件的测量过程本身还应该依赖于父控件的一些布局约束,比如:
- 1.父控件固定宽高只有
{y}px"; - 2.父控件高度为wrap_content(包裹内容),子控件设置为layout_height="match_parent";
- 3.父控件高度为match_parent(填充),子控件设置为layout_height="match_parent";
- 这些情况下,因为无法计算出准确控件本身的宽高值,简单的通过setMeasuredDimension()函数似乎不可能达到测量控件的目的,因为 子控件的测量结果是由父控件和其本身共同决定的,而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec类。
- 1.父控件固定宽高只有
- 测量流程中对布局的设计是通过MeasureSpec类来对其进行描述
- 在设计的过程中,我们将布局要求分成了2个属性。测量大小 意味着控件需要对应大小的宽高,测量模式 则表示控件对应的宽高模式:
- UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;日常开发中自定义View不考虑这种模式,可暂时先忽略;
- EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;这里我们理解为控件的宽或者高被设置为 match_parent 或者指定大小,比如20dp;
- AT_MOST:子元素至多达到指定大小的值;这里我们理解为控件的宽或者高被设置为wrap_content。
- MeasureSpec用一个32位int值来表示布局要求描述
- 前2位代表了测量模式,后30位则表示了测量的大小,对于模式和大小值的获取,只需要通过位运算即可。
- 以宽度举例来说,若我们设置宽度=5px(二进制对应了101),那么mode对应EXACTLY,在创建测量要求的时候,只需要通过二进制的相加,便可得到存储了相关信息的int值:
image - 而当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可,【measureSpec & MODE_MASK】如下图:
image - 想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可,【measureSpec & ~MODE_MASK】如下图:
image
3.3 measure测量流程
3.3.1 测量单个控件
- 只考虑单个控件的测量,整个过程需要定义三个重要的函数,分别为:
- final void measure(int widthMeasureSpec, int heightMeasureSpec):执行测量的函数;
- void onMeasure(int widthMeasureSpec, int heightMeasureSpec):真正执行测量的函数,开发者需要自己实现自定义的测量逻辑;
- final void setMeasuredDimension(int measuredWidth, int measuredHeight):完成测量的函数;
- 1.measure()入口函数:标记测量的开始
- 首先父控件需要通过调用子控件的measure()函数,并同时将宽和高的 布局要求 作为参数传入,标志子控件本身测量的开始:
// 这个是父控件的代码,让子控件开始测量 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- 对于View的测量流程,其必然包含了2部分:公共逻辑部分 和 开发者自定义测量的逻辑部分,为了保证公共逻辑部分代码的安全性,设计者将measure()方法配置了final修饰符:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // ... 公共逻辑 // 开发者需要自己重写onMeasure函数,以自定义测量逻辑 onMeasure(widthMeasureSpec, heightMeasureSpec); }
- 开发者不能重写measure()函数,并将View自定义测量的策略通过定义一个新的onMeasure()接口暴露出来供开发者重写。
- 2.onMeasure()函数:自定义View的测量策略
- onMeasure()函数中,View自身也提供了一个默认的测量策略:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
- 以宽度为例,通过这样获取View默认的宽度:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
View#getSuggestedMinimumWidth(),在某些情况下(比如自身设置了minWidth或者background属性),View需要通过getSuggestedMinimumWidth()函数作为默认的宽度值 View#getDefaultSize(minWidth, widthMeasureSpec),根据 布局要求 计算出View最后测量的宽度值。根据不同的测量模式,返回的测量结果不同。
- 3.setMeasuredDimension()函数:标志测量的完成
- setMeasuredDimension(width,height)函数的存在意义非常重要,在onMeasure()执行自定义测量策略的过程中,调用该函数标志着View的测量得出了结果。
View#setMeasuredDimension(),该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高
- 最后总结一下单个View测量流程
- 经过measure() -> onMeasure() -> setMeasuredDimension()函数的调用,最终View自身测量流程执行完毕。
3.3.2 完整测量流程
- 对于一个完整的界面而言,每个页面都映射了一个View树,见微知著,了解了单个View的测量过程,从宏观的角度思考,View树整体的测量流程将如何实现?
- 1、设计思路
- 首先需要理解的是,每种ViewGroup的子类的测量策略(也就是onMeasure()函数内的逻辑)不尽相同,但整体思路都大同小异,即 遍历 测量所有子控件,根据父控件自身测量策略进行宽高的计算并得出测量结果。
- 以 竖直方向布局 的LinearLayout为例,如何完成LinearLayout高度的测量?本文抛去不重要的细节,化繁为简,将LinearLayout高度的测量策略简单定义为 遍历获取所有子控件,将高度累加 ,所得值即自身高度的测量结果——如果不知道每个子控件的高度,LinearLayout自然无法测量出本身的高度。
- 因此对于View树整体的测量而言,控件的测量实际上是 自底向上 的,正如文章开篇 整体思路 一节所描述的:对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。
- 因为子控件的测量逻辑受限于父控件传过来的 布局要求(MeasureSpec), 因此整体逻辑应该是:
- 1.测量开始时,由顶层的父控件将布局要求传递给子控件,以通知子控件开始执行测量;
- 2.子控件根据测量策略计算出自身的布局要求,再传递给下一级的子控件,通知子控件开始测量,如此往复,直至到达最后一级的子控件;
- 3.最后一级的子控件测量完毕后,执行setMeasuredDimension()函数,其父控件根据自己的测量策略,将所有child的宽高和布局属性进行对应的计算(比如上文中LinearLayout就是计算所有子控件高度的和),得到自己本身的测量宽高;
- 4.该控件通过调用setMeasuredDimension()函数完成测量,这之后,它的父控件再根据其自身测量策略完成测量,如此往复,直至完成顶层级View的测量,自此,整个页面测量完毕。
- 经典的 递归思想,1、2步骤,开始测量的通知自顶至下,我们称之为测量步骤的 递流程;3、4步骤,测量完毕的顺序却是自底至顶,我们称之为测量步骤的 归流程。
- 2、递流程的实现
- 在整个递流程中,MeasureSpec所代表的 布局要求 占有至关重要的作用,了解了它在这个过程中的意义,也就理解了为什么我们常说 子控件的测量结果是由父控件和其本身共同决定的。
- 依然以 竖直方向布局 的LinearLayout为例,我们需要遍历测量其所有的子控件,因此,在onMeasure()函数中,第一次我们编码如下:
// 1.0版本的LinearLayout protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 1.通过遍历,对每个child进行测量 for(int i = 0 ; i < getChildCount() ; i++){ View child = getChildAt(i); // 2.直接测量子控件 child.measure(widthMeasureSpec, heightMeasureSpec); } // ... // 3.所有子控件测量完毕... // ... }
- 思考,若父布局传过来大小的是屏幕的高度,那么将其作为参数直接执行child.measure(widthMeasureSpec, heightMeasureSpec),让子控件直接开始测量,是合理的吗?
- 答案当然是否定的,试想这样一个简单的场景,若LinearLayout本身设置了padding值,那么子控件的最大高度便不能再达到heightMeasureSpec中size的大小了,但是如果像上述代码中的步骤2一样,直接对子控件进行测量,子控件就可以从heightMeasureSpec参数中取得屏幕的高度,通过setMeasuredDimension()将自己的高度设置和父控件高度一致——这导致了padding值配置的失效,并不符合预期。
- 需要额外设计一个可重写的函数,用于自定义对child的测量:计算子控件的布局要求,并把新的布局要求传给子控件,再让子控件根据新的布局要求进行测量,这样就解决了上述的问题,由此也说明了为什么 子控件的测量结果是由父控件和其本身共同决定的。
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { // 获取子元素的布局参数 final LayoutParams lp = child.getLayoutParams(); // 通过padding值,计算出子控件的布局要求 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 将新的布局要求传入measure方法,完成子控件的测量 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
- getChildMeasureSpec()函数的作用是根据父布局的MeasureSpec和padding值,计算出对应子控件的MeasureSpec,因为这个函数的逻辑是可以复用的。
- 这个函数才是子控件的测量结果是由父控件和其本身共同决定的 最直接的体现,同时,在不同的布局模式下(match_parent、wrap_content、指定dp/px),其对应子控件的布局要求的返回值亦不同。
// 2.0版本的LinearLayout protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 1.通过遍历,对每个child进行测量 for(int i = 0 ; i < getChildCount() ; i++){ View child = getChildAt(i); // 2.计算新的布局要求,并对子控件进行测量 measureChild(child, widthMeasureSpec, heightMeasureSpec); } // ... // 3.所有子控件测量完毕... // ... }
- 3、归流程的实现
- 所有子控件测量完毕,接下来 归流程 的实现就很简单了,将所有child的height进行累加,并调用 setMeasuredDimension()结束测量即可:
// 3.0版本的LinearLayout protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 1.通过遍历,对每个child进行测量 for(int i = 0 ; i < getChildCount() ; i++){ View child = getChildAt(i); // 2.计算新的布局要求,并对子控件进行测量 measureChild(child, widthMeasureSpec, heightMeasureSpec); } // 3.完成子控件的测量,对高度进行累加 int height = 0; for(int i = 0 ; i < getChildCount() ; i++){ height += child.getMeasuredHeight(); } // 4.完成LinearLayout的测量 setMeasuredDimension(width, height); }
04.performLayout布局
4.1 performLayout源码
- 从ViewRootImpl类中分析performLayout测量,这里是测量的入口
ViewRootImpl#performLayout(),具体看一下host.layout(),这个host就是一开始的Activity的mDector根组件 View#layout(),看源码执行了onLayout方法,host是一个FrameLayout,所以跟measure类似的,看一下FrameLayout的onLayout方法的实现 FrameLayout#onLayout(),调用了layoutChildren方法 FrameLayout#layoutChildren(),在这个方法中,遍历执行View的layout方法,若是ViewGroup则执行具体的ViewGroup的layout方法,若是View,则执行View的layout方法 View#layout(),经过layout方法,如果是View组件的话就已经将View组件的位置信息计算出来并保存在对象的成员变量中
4.2 layout设计思路
- 布局设计的总体思路,负责对所有子控件进行对应策略的布局,这就是 布局流程(layout)。
- 1.对于叶子节点的View而言,其本身没有子控件,因此一般情况下仅需要记录自己在父控件的位置信息,并不需要处理为子控件布局的逻辑;
- 2.1对于整体的布局流程而言,子控件的位置必然交由父控件布置,和 测量流程 一样,Android中布局流程中也使用了递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件开始布局时,会通过自身的布局策略依次计算出每个子控件的位置。
- 2.2值得一提的是,为了保证控件树形结构的 内部自治性,每个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置。位置计算完毕后,作为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当所有控件都布局完毕,整个布局流程结束。
4.3 layout布局流程
4.3.1 单个View的布局流程
- 思考一个问题,布局流程的本质是测量结束之后,将每个子控件分配到对应的位置上去——既然有子控件,那说明进行布局流程的主体理应是ViewGroup,那么作为叶子节点的单个View来说,为什么也会有布局流程呢?
- 读者认真思考可以得出,布局流程实际上是一个复杂的过程,整个流程主要逻辑顺序如下:
- 1.决定是否需要重新进行测量流程onMeasure();
- 2.将自身所在的位置信息进行保存;
- 3.判断本次布局流程是否引发了布局的改变;
- 4.若布局发生了改变,令所有子控件重新布局;
- 5.若布局发生了改变,通知所有观察布局改变的监听发送通知。
- 整个布局过程中,除了4是ViewGroup自身需要做的,其它逻辑对于View和ViewGroup而言都是公共的——这说明单个View也是有布局流程的需求的。
- 现在将整个布局过程定义三个重要的函数,分别为:
- void layout(int l, int t, int r, int b):控件自身整个布局流程的函数;
- void onLayout(boolean changed, int left, int top, int right, int bottom):ViewGroup布局逻辑的函数,开发者需要自己实现自定义布局逻辑;
- void setFrame(int left, int top, int right, int bottom):保存最新布局位置信息的函数;
- 1.layout函数:标志布局的开始
- 站在单个View的角度,首先父控件需要通过调用子控件的layout()函数,并同时将子控件的位置(left、right、top、bottom)作为参数传入,标志子控件本身布局流程的开始
// 伪代码实现 public void layout(int l, int t, int r, int b) { // 1.决定是否需要重新进行测量流程(onMeasure) if(needMeasureBeforeLayout) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec) } // 先将之前的位置信息进行保存 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // 2.将自身所在的位置信息进行保存; // 3.判断本次布局流程是否引发了布局的改变; boolean changed = setFrame(l, t, r, b); if (changed) { // 4.若布局发生了改变,令所有子控件重新布局; onLayout(changed, l, t, r, b); // 5.若布局发生了改变,通知所有观察布局改变的监听发送通知 mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } }
- 通过伪代码的方式对布局流程进行了描述,实际上View本身的layout()函数内部虽然多处不同,但核心思想是一致的——layout()函数实际上代表了控件自身布局的整个流程,setFrame()和onLayout()函数都是layout()中的一个步骤。
- 2.setFrame函数:保存本次布局信息
- 为什么需要保存布局信息?因为我们总是有获取控件的宽和高的需求——比如接下来的onDraw()绘制阶段;而保存了布局信息,就能通过这些值计算控件本身的宽高。
- 保存最新的布局信息,现在将目光转到mLeft、mTop、mRight、mBottom四个变量上。这四个变量对应的自然是View自身所在的位置,那么View是如何通过这四个变量描述控件的位置信息呢?
- 3.相对位置和绝对位置
image image - 这时候不可避免的会面临另外一个问题,这个mLeft、mTop、mRight、mBottom的值所对应的坐标系是哪里呢?
- 这里需要注意的是,为了保证控件树形结构的 内部自治性,每个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置
- 反过来想,如果这些位置信息是以屏幕坐标系为准,那么就意味着每个叶子节点的View会持有保存从根节点ViewGroup直到自身父ViewGroup每个控件的位置信息,在计算布局时则更为繁琐,很明显是不合理的设计。
- 4.onLayout函数:计算子控件的位置
- 对于叶子节点的View而言,其并没有子控件,因此一般情况下并没有为子控件布局的意义(特殊情况请参考AppCompatTextView等类),因此View的onLayout()函数被设计为一个空的实现。
- 而在ViewGroup中,不同类型的ViewGroup有不同的布局策略,这些布局策略的逻辑各不相同,因此该方法被设计为抽象接口,开发者必须实现这个方法以定义ViewGroup的布局策略。
- 以LinearLayout为例,其布局策略为 根据排布方向,将其所有子控件按照指定方向依次排列布局。
4.3.2 完整布局流程
- 整体思路是,对于一个完整的界面而言,每个页面都映射了一个View树,最顶端的父控件开始布局时,会通过自身的布局策略依次计算出每个子控件的位置。位置计算完毕后,作为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当所有控件都布局完毕,整个布局流程结束。
- 唯一需要注意的是,开发者必须实现onLayout()函数以定义ViewGroup的布局策略,这里以 竖直布局 的LinearLayout的伪代码为例:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTop; int childLeft; // 遍历所有子View for (int i = 0; i < count; i++) { // 获取子View final View child = getVirtualChildAt(i); // 获取子View宽高,注意这里使用的是 getMeasuredWidth 而不是 getWidth final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); // 令所有子控件开始布局 setChildFrame(child, childLeft, childTop, childWidth, childHeight); // 高度累加,下一个子View的 top 就等于上一个子View的 bottom ,符合竖直线性布局从上到下的布局策略 childTop += childHeight; } } private void setChildFrame(View child, int left, int top, int width, int height) { // 这里可以看到,子控件的mRight实际上就是 mLeft + getMeasuredWidth() // 而在getWidth()函数中,mRight-mLeft的结果就是getMeasuredWidth() // 因此,getWidth() 和 getMeasuredWidth() 是一致的 child.layout(left, top, left + width, top + height); }
- 在整个布局流程的设计中,设计者将流程中公共的业务逻辑(保存布局信息、通知布局发生改变的监听等)通过layout()函数进行了整合,同时,将ViewGroup额外需要的自定义布局策略通过onLayout()函数向外暴露出来,针对组件中代码的可复用性和可扩展性进行了合理的设计。
image
05.performDraw绘制
5.1 performDraw源码流程
- 从ViewRootImpl类中分析performDraw测量,这里是绘制的入口
ViewRootImpl#performDraw(),看源码可知调用了draw(fullRedrawNeeded) ViewRootImpl#draw(),调用了mView的draw方法,这里的mView是我们的mDector,看一下draw方法的具体实现 ViewRootImpl#drawSoftware(),调用mSurface.lockCanvas方法获取一个Canvas对象,然后drawColor,translate,setScreenDensity等等。最后调用mView.draw(canvas)。 ViewRootImpl#drawSoftware()#mView.draw(canvas),由于mView是我们的mDector,因此这里可以看FrameLayout的draw方法 FrameLayout#draw(),如果包含子View,那么也会执行子View的draw方法。
- 从上述源码可以看到ViewRootImpl有一个Surface属性,当界面绘制时,就调用mSurface.lockCanvas方法获取一个Canvas对象传递个View递归绘制。ViewRootImpl简易类图如下。
image
- 然后看一下Surface的源码
Surface#lockCanvas,这里调用了nativeLockCanvas方法 android_view_Surface#nativeLockCanvas()。
//frameworks\base\core\jni\android_view_Surface.cpp static jlong nativeLockCanvas(JNIEnv* env, jclass clazz, jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) { sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject)); Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj); // 给Canvas设置Bitmap nativeCanvas->setBitmap(bitmap); sp<Surface> lockedSurface(surface); lockedSurface->incStrong(&sRefBaseOwner); return (jlong) lockedSurface.get(); }
- 我们的界面像素数据保存在Surface中,这个Surface就是在ViewRootImpl中创建的。
image
5.2 draw绘制设计
- 绘制整体的设计思路:
- 大部分view就是先绘制必要的背景,然后调用onDraw绘制自身内容,绘制完后如果是ViewGroup,还会调用dispatchDraw绘制子view,最后绘制前景一些UI即可;对于一些特殊view,只是多加了绘制渐变框的一步(使用保存图层、绘制图层、恢复画布的做法)。
- 关于ViewGroup中dispatchDraw设计思路
- 自上而下、一层层地传递下去,直到完成整个View树的draw过程。
image
5.3 draw绘制流程
- ViewRootImpl.performTraversals()真正的绘制
- 调用relayoutWindow():
- 创建用户java层的surface:只有用户提供的画面数据;
- 创建native层的surface:包含用户提供的画面数据(java层的surface)+系统的画面数据(状态栏,电池、wifi等等);
- 创建完surface后:依次调用:performMeasure(对应view的onMeasure)、performLayout(onLayout)、performDraw(onDraw);
- 在performDraw()中:
- 将view的数据传至native层的surface
- surface中的canvas记录数据
- 生成bitmap图像数据(此时数据是在surface中)
- 将surface放入队列中;生产者消费者模式;
- 通知surfaceflinfer进程去队列中取surface数据
- surfaceflinfer拿到不同的surface,进行融合,生成bitmap数据
- 将bitmap数据放入framebuffer中,进行展示
06.View绘制流程总结下
6.1 Activity布局绘制
- 想要弄清楚View是怎么绘制的得先弄明白View是怎么创建出来的。我们先来看下View的创建流程。
image
- 总结如下所示:
- Activity执行onResume之后再ActivityThread中执行Activity的makeVisible方法。
- View的绘制流程包含了测量大小,测量位置,绘制三个流程;
- Activity的界面绘制是从mDoctor即根View开始的,也就是从mDoctor的测量大小,测量位置,绘制三个流程;
- View体系的绘制流程是从ViewRootImpl的performTraversals方法开始的;
- View的测量大小流程:performMeasure --> measure --> onMeasure等方法;
- View的测量位置流程:performLayout --> layout --> onLayout等方法;
- View的绘制流程:onDraw等方法;
- View组件的绘制流程会在onMeasure,onLayout以及onDraw方法中执行分发逻辑,也就是在onMeasure同时执行子View的测量大小逻辑,在onLayout中同时执行子View的测量位置逻辑,在onDraw中同时执行子View的绘制逻辑;
- Activity中都对应这个一个Window对象,而每一个Window对象都对应着一个新的WindowManager对象(WindowManagerImpl实例);
07.View刷新操作
7.1 View如何显示在屏幕
- 一个 view 究竟是如何显示在屏幕上的?
- 一般都比较了解 view 渲染的三大流程,但是 view 的渲染远不止于此:此处以一个通用的硬件加速流程来表征
image
- 关于整个View显示在屏幕上的流程如下
- Vsync 调度:很多同学的一个认知误区在于认为 vsync 是每 16ms 都会有的,但是其实 vsync 是需要调度的,没有调度就不会有回调;
- 消息调度:主要是 doframe 的消息调度,如果消息被阻塞,会直接造成卡顿;
- input 处理:触摸事件的处理;
- 动画处理:animator 动画执行和渲染;
- view 处理:主要是 view 相关的遍历和三大流程;
- measure、layout、draw:view 三大流程的执行;
- DisplayList 更新:view 硬件加速后的 draw op;
- OpenGL 指令转换:绘制指令转换为 OpenGL 指令;
- 指令 buffer 交换:OpenGL 的指令交换到 GPU 内部执行;
- GPU 处理:GPU 对数据的处理过程;
- layer 合成:surface buffer 合成屏幕显示 buffer 的流程;
- 光栅化:将矢量图转换为位图;
- Display:显示控制;
- buffer 切换:切换屏幕显示的帧 buffer;