绘制流程
硬件驱动
主要包括两大部分:渲染和合成;
渲染是由GPU将所需要的画面分层,把每层的绘图指令转化为二进制数组
绝大多数Andorid设备的合成是由封装在GPU中的DPU芯片承担,DPU将GPU输出的所有图层进行合成(执行计算“脏区域”、格式转换、处理缩放等操作)
由于DPU合成数量的限制,亦或者没有不单独配置合成芯片时,那么图层的合成工作该如何进行处理?此时需要实现HWC接口,这样就可以满足sf进程进行图层合成时的工作分配问题。
Android实现
1.GraphicBuffer
每当应用有显示需求时,应用会向系统申请一块GraphicBuffer
内存,GraphicBuffer可以理解为带有
硬件的互斥锁的一块同步内存,
他会被GPU
、CPU
、DPU
三个不同的硬件访问,
一个GraphicBuffer对象完整的生命周期大概是这样:
- 渲染阶段:应用有绘图需求了,由GPU分配一块内存给应用,应用调用GPU执行绘图,此时使用者是GPU
- 合成阶段:GPU渲染完成后将图层传递给sf进程,sf进程决定由谁来合成,hwc或者GPU
- 如果使用GPU合成,那么此时buffer的使用者依旧是GPU
- 如果使用hwc合成,那么此时buffer的使用者是hwc
- 显示阶段:所有的buffer在此阶段的使用者都是hwc,因为hwc控制着显示芯片
2.BufferQueue
既然GraphicBuffer是渲染合成的实际执行者,那应该如何对GraphicBuffer进行管理呢?
libgui组件库中的BufferQueue则是用来管理GraphicBuffer的,其内部
封装了GraphicBuffer的队列,并对外提供了GraphicBuffer对象出列/入列的接口。 BufferQueue还为每个GraphicBuffer对象包装了几种不同的状态,每个Buffer的一生,就是在不断地循环FREE
->DEQUEUED
->QUEUED
->ACQUIRED
->FREE
这个过程
设计上,BufferQueue使用了生产者/消费者模式,绝大多数的情况下,APP作为GraphicBuffer的生产者,sf进程作为GraphicBuffer的消费者,它们俩共同操作一个buffer队列
状态转换过程:
生产者:APP进程
1、producer->dequeueBuffer()
从队列取出一个状态为“FREE”的buffer,此时该buffer状态变化为:FREE->DEQUEUED
2、producer->queueBuffer()
将渲染完成的buffer入列,此时该buffer状态变化为:DEQUEUED->QUEUED
消费者:sf进程
1、consumer->acquireBuffer()
从队列中取出一个状态为“QUEUED”的渲染完的buffer准备去合成送显,此时该buffer
的状态变化为:QUEUED->ACQUIRED
2、consumer->releaseBuffer()
buffer内容已经显示过了,可以重新入列给APP使用了,此时该buffer的状态变化为:
ACQUIRED->FREE
3.Surface
对于应用开发者来说,Surface是实际和我们打交道的核心类,Surface作为图像的生产者,持有BufferQueue的引用,对图像进行渲染和合成。
以2D绘图的流程来举例,可参考View中的Ondraw方法:
1、需要显示图形时,首先创建一个Surface对象
2、调用Surface#lockCanvas()获取Canvas对象
(内部调用C的方法,申请一块图形
GraphicBuffer
,后续所有的绘图结果都会写入这块内存中)3、调用Canvas的draw开头的函数执行一系列的绘图操作
4、调用Surface#unlockCanvasAndPost()将绘制完成的图层提交,等待下一步合成显示
绘制三部曲
绘制从ViewRootImpl#doTraversal()
方法开始执行绘制,在ViewRootImpl中会
申请surface,会跟从视图
一层层向下遍历
Android 如何绘制视图
绘制流程分为三步:测量、布局、绘制,分别对应onMeasure()、onLayout()、onDraw(),
- onMeasure():测量当前控件的大小,为正式布局提供建议(只是建议,至于是否使用,要看onLayout()函数
- onLayout():一般使用layout()函数对所有子控件进行布局
- onDraw():根据布局的位置绘图
测量
测量是view树自顶向下的遍历,确定每个 ViewGroup 和 View 元素的尺寸。
1.View的测量过程
过程通过view中的measure(int widthMeasureSpec, int heightMeasureSpec)方法(ViewGruop调用),再调用 setMeasuredDimension()
方法将测量的结果保存起来。
其中方法中的widthMeasureSpec
和 heightMeasureSpec
参数则是由父 View 和子 View 的尺寸共同作用得到的32位二进制数
UNSPECIFIED
:未指定模式,可以随意设置自己的尺寸信息。比如当你的父视图是可以纵向滚动的ScrollView
,那子视图的高度大小对于父视图来说没有意义。无论你多高(即使超出屏幕),都可以通过滑动屏幕来查看EXACTLY
:精确模式。当你收到此模式时,表示父视图希望你就这么大(不要小于或大于给定的大小),通常在 xml 中指定大小或者设为match_parent或具体数值
时会收到EXACTLY
模式AT_MOST
:最大模式。当你收到此模式时,表示你可以在父视图给定的范围内随意发挥,但最好不要超过父视图给你的大小,通常在 xml 中设为wrap_content
时会收到AT_MOST
模式
大多数情况下View的MeasureSpec 的创建是父视图通过调用 getChildMeasureSpec()
方法,由父视图的 SpecMode 和 子View自身的LayoutParams 共同决定
但是ScrollView的子view是一个例外,因为在测量ScrollView的子view时没有调用 getChildMeasureSpec()
方法,而是直接调用 makeMeasureSpec()
方法为子 View 指定了测量模式与大小
void measureChild() {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,lp.width);
//高度指定为parentSize
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSize,MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
2.ViewGroup的测量过程
ViewGroup 的 onMeasure()
方法没有默认实现,但是, Android 为 ViewGroup 准备了几个测量子 View 的方法.其中都会调用getChildMeasureSpec方法,通过View中LayoutParams的长宽来绘制(View 有 top
、bottom
、left
、right
和 padding
这几个属性,记录的是这个 View 在屏幕坐标系中的绝对位置,但它这几个属性只有在 layout
阶段以后才会有具体的值)
//测量子视图
void measureChild()
//测量子视图并计算其Margin
//想要在代码中动态的修改 View 的 margin 属性,记得强转LayoutParams
//为 MarginLayoutParams 类型再进行操作
void measureChildWithMargins()
//获取子View的MeasureSpec,对于ViewGroup来说,这是非常重要的一个方法!!!
int getChildMeasureSpec(int spec, int padding, int childDimension)
3.多次执行onMeasure()
1)首次加载 Activity 时,View 都会执行2次 onMeasure()
方法的
2)Dialog 或者是 Dialog 主题的 Activity时会执行3次 performTraversals()
方法,在 measureHierarchy()
方法中会执行2或3次测量,再加上述图片中视图尺寸发生变化还会再执行一次,所以每发起requestLayout请求时,会执行3或4次测量,3次调用则是9或12次
3)Window 权重导致多次调用 场景较少,待理解
4)由 ViewGroup 自身发起调用
例如:
LinearLayout在设置权重或本身设置为自适应且子View设置填充父布局情况时,会进行多次测量
FrameLayout当自身宽高有一个不确定(自适应),且至少有两个子View的宽或高为填充父布局,会进行多次测量
可参考: 每日一问 | onMeasure()多次执行原因?-玩Android - wanandroid.com 密码 :2020
布局
布局阶段的任务量主要是在 ViewGroup ,父视图负责把子 View 们按照LayoutParams 规则摆放好
onLayout()是实现所有子控件布局的函数。所有控件的根节点是ViewRoot,ViewRoot会先通过setFrame()方法设置自身吃尺寸,然后调用onLayout()方法设置子控件布局。
view类的onLayout()是个空方法,viewGroup的onLayout()是个抽象方法需要子类自行实现。核心代码就是拿到onmeasure()中拿到的指导长宽的数值,通过child.layout()方法定义子views所在位置。
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(mChanged, mLeft, mTop, mRight, mBottom);
绘制
绘制是先执行 ViewGroup 绘制流程,再执行子 View 的绘制流程,先绘制的内容会被后绘制的内容覆盖掉。如果开启硬件加速,会影响绘制执行的链路
- 调用
drawBackground()
方法画 ViewGroup 的背景 - 调用自身的
onDraw()
方法执行 Canvas 绘图逻辑 - 调用
dispatchDraw()
通知子 View 执行draw()
- 调用
onDrawForeground()
方法画视图的前景
基本用法
1.layout_constraintBaseline_toBaselineOf
Baseline指的是文本基线,可以实现两个textsize不同的控件对齐。
2.margin需要对控件所需设置的边有约束后,才能进行距离的设置;可以配合gonemargin灵活使用
3.bias 偏移量,需要先行约束后,才能生效,例如对横向设置bias,则需要设置start to 和end to。
4.circle 设置圆心半径角度,进行约束控制位置
5.chainstyle 将多个控件设置为一条链的前提下,可以对多个控件所形成的链路自动形成间距。(三个模式)
6.长度宽度 若设置为0dp后,可以配合layout_constraintWidth_default属性对其长款进行设置,也可以配合weight属性设置权重(类似linearlayout)。
- layout_constraintWidth_default
- spread:默认,会尽量填充父布局。
- wrap:自适应
- percent:百分比,配合width_percent使用
7.当宽或高至少有一个尺寸被设置为0dp时,可以通过layout_constraintDimensionRadio 设置宽高比
宽设置为0dp,宽高比设置为1:1,这个时候textview1是一个正方形,效果如下:
除此之外,在设置宽高比的值的时候,还可以在前面加W或H,分别指定宽度或高度限制。例如:
layout_constraintDimensionRatio="H,2;3 指的是 高度被约束
layout_constraintDimensionRatio="W,2:3 指的是 宽度被约束
8.guideline控件,辅助线,可拖动,可通过orientation设置横纵,方便设置一组控件对齐的基准线
9.Group 可以把多个控件归为一组,方便隐藏或者显示一组控件