android View原理(View树遍历,View重绘)

屏幕绘图基础

Android 中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户需要绘制时,首先请求服务端创造一个窗口,然后在窗口中进行具体的试图内容绘制;对于每个客户而而言,他们都感觉自己独占了屏幕,而对于服务端而言,它会给每一个客户端窗口分配不同的层值,并根据用户的交互情况动态改变窗口的层值,这就给用户造成了所谓的前台窗口和后台窗口的概念:

Android的屏幕绘制架构如下图:

  1. SufaceFlinger进程:简称sf,该进程在系统开机时自动启动,在init.rc中定义,他的作用就是给每个客户端分配窗口,在程序中用Surface类来表示这个窗口,即每个窗口都是一个平面,而每个平面都在程序中都会对应一块屏幕缓冲区.
  2. SystemServer进程:前面已经重点讲了该进程的相关信息了,他的zygote进程孵化出的第一个进程,里面加载的是所有android系统运行所需的各种系统服务,当然最重要的就是AMS和WMS,而WMS正是屏幕绘图服务端最重要的窗口管理服务,客户端应用程序要请求窗口展示直接与WMS打交道,然后WMS进行相关的窗口管理,并通过sf的客户端驱动接口和sf打交道,从而完成窗口内容的绘制:
  3. Surface/Cancas类:主要用来记录窗口的宽,高,位置,层值等相关信息,WMS就是用他来和sf客户端驱动打交道(新版本中实际是通过SurfaceSession来作为纽带)具体的是当Surface初始化时会通过sf客户端接口创建真正窗口需要的屏幕缓冲区;完成之后应用程序通过lockCanvas()来获得一个Canvas对象,再之后便可以调用各式各样的api完成具体内容的绘制;
    注意:Suface对象并不能由应用程序直接初始化,他只对SDK内部开放,主要在ViewRootImpl中创建;不过android系统提供了一个SufaceVIew可以让应用程序来间接的使用suface;
  4. Skia图形库:用C/C++编写的图形驱动库,用来完成各种平面绘制,Canvas类中的各种drawXXX方法实际上都是交由Skia库执行的;

Cannas/Drawable/Paint的关系与区别

Canvas:画布,所有的绘图都需要经过他在调用底层Skia完成具体绘制,画布一般是通过Sueface完成屏幕缓冲区初始化之后获取;
Drawable:一个抽象类,其子类代表某个特定的图案,他仅仅是一个功能类,无法完成具体的绘图工作,但是它提供了一个abstact方法draw(Canvas canvas),通过他直接转交给canvas实例处理,你可以根据需要实现的自定义的Drawable;

Paint:画笔,用来保存图案绘制时所用的颜色,样式(是否有阴影,粗细,圆角,字体,对齐方式等),一般在canvas.drawXXX中作为参数进行使用;

View树遍历

View状态的分类

在View视图的定义了多种和界面效果相关的状态,比如拥有焦点Focused,按下Pressed等,不同的状态一般回想时处不同的压面效果,而视图的状态会因为用户的操作而改变,一般通过xml文件中的selector标签来申明不同状态下的背景图;所有的状态码位于StateListDrawable中,常用的状态码包括:

  1. enable:当前View是否可用,这个状态可由setEnable()改变,他完全有开发者控制;当状态不可用时,View将不会响应任何事件.
  2. focuse:当前View是否正拥有焦点,一个窗口中只能有一个View拥有焦点,一般随用户操作而主动改变,该状态主要是针对按键的,因为所有的按键消息都将派发给focusd视图.
  3. pressed:当前View是否正在被按下,主要是针对触摸消息的,一般用户按下视图会有一个明显的变化,也是随用户操作而动态改变.
  4. selected:当前View是否被选中,一个窗口中可以有多个视图处于选中状态;开发者可以通过setSelected()改变,他完全由开发者控制.

导致View树重新遍历的总体诱因

遍历View树意味着整个View需要重新对其包含的子View分配大小并重绘;一般情况下导致重新遍历的原因有三个:其一,视图内部状态发生变化,比如显示属性从GONE到VISIBLE;其二,ViewGroup中添加或删除了视图导致需要重新为子视图分配位置,其三,视图本身大小进行变化,比如TextView中的文本内容变多或者变少了;
在代码层面这是三种情况最后都会直接或间接调用到View中的是三个函数:requsetLayout / requestFucus / invalidate ;由于是View树遍历,所以最后都会执行到最顶级父视图中的ViewRootImpl.scheduleTraversals();该方法内,系统会发起一个异步消息,然后在异步消息执行过程中performTraversals()完成具体的View树遍历.
View中超多的数形变量如何管理?
在庞大的View类中涉及到非常多的状态码,比如是否可用,是否处于按压状态,等等,View树在遍历重绘时会根据不同的变量值来进行相应的操作,为此View中引入了bit标示位来管理.
其中mViewFlags变量主要用来保存和视图状态相关的值,比如是否可单击,是否可双击,是否可用,是否拥有焦点等;
mPrivateFlages变量主要用来保存和内部逻辑相关的属性,比如是否需要重新分配位置,是否需要重绘,是否刷新View缓存
注意:这两个变量之间是有紧密联系的,经常会需要两个变量同时设置某些状态值,可以参见setFlags()方法:

  • requestLayout()
    该方法的执行过程很简单,因为当View树重新进行布局时,总是重新给所有视图进行布局,而不像重绘是可以指定只绘某一小区域的;
    从代码层面他只是为mPrivateFlags变量添加一个FORCE_LAYOUT标识而已,然后逐层请求mParent.requestLayout();详见下图:


    image
  • invalidate()
    该方法的作用是请求View树重绘;视图极其父视图在界面上是层次先后显示的,父视图位于子视图的下面,绘制过程中,首先绘制最底层的根视图,然后绘制其包含的子视图,子视图若是ViewGroup,则继续绘制其子视图.如此迭代至没有子视图为止;
    在具体的重绘过程中,一般不会对所有视图都进行重绘,而只是绘制那些需要绘制的视图,那如何找到需要绘制的区域呢?这就是invalidate方法要完成的事情.
    大致的思路是:当View需要重绘时会给mPrivateFlags变量添加DRAWN表示,然后根据带有该标识的边界一起确定最终要绘制的举行区块,


    image.png

    代码的具体执行过程:

  1. View.invalide()中设置必要的状态标识之后,会执行到mParent.invalidateChild();这里的mParent有两种情况,一种是有父视图ViewGroup,另一种是已经到顶层为ViewRootlmpl
  2. 若是ViewGroup,会执行完invalidataChildINparent()之后继续调用mParent.invalidateChildInParent();
  3. 最终调用到ViewRootImpl.invalidateChildInParent(),进而执行scheduleTraversals();注意:这里会提前判断mWillDramSoon局部变量值,若当前已经执行了performTraversals()遍历重绘了,那就不会调用scheduleTravesals(),也就是说不会发起重绘的异步消息了,但View中设置各种状态值仍然是有效的,只会在下次重绘时生效.
  • scheduleTranversals()
    该方法会在多个地方被调用,比如requestLayout() / invalidate()中,而我们又会经常看到连续调用这两个方法的情况,那这样岂不是会连续发起两次VIew树重回遍历请求?其实不会的,因为上面已经说到,scheduleTranversals方法内设置了一个局部变量mTranversalSchedduled,若先执行了requestLayout(),那此时mTranversalSchedduled的值由false变为true,接着调用invalidate()时,判断mTranversalSchedduled为true那么不会再重复发布一个异步消息了.


    image.png
  • performTraversals()
    该方法是系统内进行View树遍历并进行重绘的核心方法,内部逻辑还是非常复杂,大致流程为:根据之前设置好的各种状态值,判断是否重新计算视图大小(Measure),是否重新分配视图的位置(Layout)是否需要重绘(Draw)具体如下:


    image.png

View重绘过程

计算视图大小的过程(Measure)
视图大小,准确的来说是视图的布局大小,我们在Layout.xml中的每个UI控件设置的layout_width/layout_height两个属性被用来设置父视图给当前试图分配的窗口大小,为了开发方便和对不同的屏幕分辨率的兼容适配对这两个参数的赋值一般都是用相对值,比如WRAP_CONTENT/MATCH_PARENT,计算视图布局大小的过程就是本质上就是吧视图布局是使用的"相对值"转换为具体值的过程;

Measure递归调用过程

View系统启动measure过程是从ViewRootImpl中调用host.measure()开始的,详见下图:


image.png

从上图中可以看出measure过程中主要就是从顶层父视图向子视图的递归调用view.measure(),注意以下几点:

  1. View.measure()该方法是final的,不允许重载,View子类只能通过重载onMeasure()来完善自己的逻辑.
  2. MeasureSpec测量规格在measure过程经常作为输入参数,该值为int型,其值有两部分组成,高16位代表规格specMode,低16位代表具体尺寸,其中specMode只有种值:
    1)MeasureSpec.EXACTLY:确定模式,即父视图希望子视图的大小是确定的,由specSize决定;
    2)MeasureSpec.AT_MOST:最多模式,即父视图希望子视图的大小最多是specSize指定的值;
    3)MeasureSpec.UNSPECIFIED:未指定模式,此时父视图完全尊重子视图的设计;
  3. 最顶层视图DecorView测量时的MeasureSpec从何而来呢?
    是在ViewRootImpl中调用 getRootMeasureSpec(..)获得,LayoutParam宽高参数均为MATCH_PARENT;
    获得的specMode就是EXACTLY,specSize为物理屏幕大小;
  4. 视图的布局大小又父视图与子视图共同决定;
    layout.xml中对含有子视图的布局器中的layout_width/layout_height属性实际扮演了2个角色;一个是和父视图一起对布局器自身进行measure操作;另一个角色是作为其子视图的父视图参与子视图的measure操作;
  5. ViewGroup类中提供了 measureChildWithMargins(…)方法,用来抽象和简化父子视图之间的padding、margin、实际内容区域间的测量和尺寸计算,让开发者可以无需关注这些公共的边界测量区域;
layout的递归调用过程

View系统启动layout过程是从ViewRootImpl中调用host.layout(…)开始的,参见下图:


image.png

从上图中可以看出layout过程也是从顶层父视图向子视图的递归调用view.layout(…),即父视图根据上一步measure子视图所得到的布局大小和布局参数,将子视图放在合适的位置上,布局参数核心指layout_gravity;注意以下几点:

  1. 与measure方法不同,View.layout(…)方法可被重载,ViewGroup.layout(…)为final的不可重载,ViewGroup.onLayout(…)为abstract的,子类必须重载,里面可以实现自己的位置分配逻辑;
  2. measure操作完成后得到的是对每个View经测量过的宽高measuredWidth/measuredHeight;
    layout操作完成之后得到的是对每个View进行位置分配后的mLeft/mTop/mRight/mBottom;
  3. layout_gravity一般出现于布局容器中,比如LinearLayout,他指的是当前容器内子视图的排列方式和顺序;
    gravity一般出现于具体的View中,比如TextView,他指的就是TextView中实际文字的位置排放方式;
  4. 凡是以layout_开头的布局参数基本都针对的是包含子视图的容器视图的,比如核心的layout_width/layout_height/layout_weight/layout_gravity,会在初始化LayoutParam时从xml中读取并转换为相应参数;
    当对一个没有父容器的View设置相关layout_开头的属性时,实际上是没有任何意义的;
Draw递归调用过程

绘制过程就是把View对象绘制到屏幕上,如果该View是一个容器ViewGroup,则需要递归绘制其所包含的所有子视图;视图中可绘制的元素包括:

  1. View背景Backgroud:每个视图都可以有一个背景,背景可以是一个颜色值,也可以是一副图片,甚至可以是任何Drawable对象;
  2. 视图自身的内容:一般由视图设计者在视图的onDraw方法中完成具体的内容绘制,比如TextView的内容就是具体的文字,若是视图容器ViewGroup,则需要递归完成子视图的具体内容绘制;
    只有这项内容是需要开发者设计并实现的,其余三项内容均由系统自动完成绘制;
  3. 渐变边框FadingEdge:其作用是为了让视图的边框看起来更有层次感,其本质就是一个Shader对象,当然可以通过配置项关闭该效果;
  4. 滚动条ScrollBar:用来显示当前滚动的位置和状态,与PC不一样,该滚动条一般不能直接按住拖动;
    View系统启动draw过程是从ViewRootImpl中调用host.draw(…)开始的,总体过程与measure/layout极其类似,即从顶级父视图开始向子视图进行递归调用view.draw(…),具体可参见下图:


    image.png
  • 下面就来看看这个过程中的几个核心方法都做了哪些具体的事情;
  1. ViewRootImpl.draw()


    image.png

    里面要特别注意的一个核心点就是会根据根视图内部的Scroller对象来调用mScroller.computeScrollOffset()判断当前视图是否还处于滚动状态,若处于滚动状态,则会进行滚动偏移量计算,并且在最后再次调用scheduleTraversals()来发送一个异步重绘请求;
    另外一点就是:Surface会按照底层驱动模式自动执行显卡模式或CPU模式;前者采用显卡来进行页面绘制,支持硬件图形加速,一般基于OpenGL实现;后者也被成为软件模式,即通过CPU及内存来模拟图形绘制,不支持硬件加速;目前的一些高端机型几乎都是显卡模式,低端机型更多采用CPU模式;

  2. View.draw()内部流程主要就是为了完成前面提到的视图中的各种具体元素的绘制,大致过程如下:
  1. 绘制背景,由于可能会出现滚动条,所以绘制时可能会涉及到Canvas画布的平移和恢复,即 canvas.translate(scrollX, scrollY);
  2. 判断是否需要显示渐变框,若不需要则直接进入后续3、4、5绘制逻辑;
  3. 绘制视图自身内容,通过回调onDraw()实现
  4. 调用dispatchDraw()绘制子视图,对于ViewGroup而言,默认已重载该方法,如有特别需求子类无需再次重载该函数;
  5. 调用onDrawScrollBars()绘制滚动条;
  1. ViewGroup.dispatchDraw()的作用是绘制父视图中包含的子视图,其本质就是给不同的子视图分配合适的画布Canvas,至于子视图具体如何绘制,则又会递归回调View.draw()方法;该方法内部将根据onLayout()中为子视图分配的具体区块调整Canvas的内部剪切区,从而让子视图认为画布是他自己独享的,坐标也是从(0,0)开始;其内部具体执行流程参见下图:


    image.png

    有几点需要注意:

  1. 区分View动画和ViewGroup布局动画:前者指的是View自身的动画,可以通过setAnimation(.)添加;而后者是专门针对ViewGroup而言的,指的是该ViewGroup在显示内部的子视图时而设置的动画,可以在layout.xml中对容器标签设置layoutAnimation属性,比如可以对LinearLayout设置子视图在显示时出现逐行显示、随机显示、或落下等不同的动画效果;
    2)在获取画布剪切区时会自动处理掉padding,让子视图获取的画布无需关注这些附加逻辑;
    3)默认情况下子视图的ViewGroup.drawChild()绘制顺序与子视图被添加的顺序一致,但开发者可以重载ViewGroup.getChildDrawingOrder()方法提供不同的顺序;
    4)当给一个子视图添加了移除动画时,该子视图会被添加到mDisappearingChildren队列中,在动画结束之前该子视图将一致存在,但此时该子视图无法被点击,也无法获得任何消息事件,仅仅是可见而已;
  1. ViewGroup.drawChild()的核心过程是为子视图分配合适的Canvas剪切区,其大小取决于child的布局大小,位置取决于child的内部滚动值、以及当前动画,其内部流程参见下图:


    image.png

    有几点需要注意:
    1)该方法在新的版本(比如4.2.2)中已经被放在View中了,ViewGroup中直接中转调用child.draw(canvas, this, drawingTime);
    这是在看android源码时需要特别注意的,View/ViewGroup之间有很多方法重载,且存在父子视图的递归调用,经常会把人搞晕,看代码时需要非常平静才行;
    2)对于ViewGroup中子视图的动画支持有两种方式,一种是通过setAnimation(.)添加,另外一种是通过重载ViewGroup.getChildStaticTransformation(View child, Transformation t)方法实现;

  2. View.onDrawScrollBars()的作用是绘制滚动条;
    1)滚动条是View的基本元素之一,每个View都可以有滚动条,只是某些视图从用户体验的角度无需显示而已,可以在layout.xml中通过scrollBarXXX相关的属性设置滚动条;
    2)滚动条可以是水平的、垂直的、或者两者均有,滚动条的展示封装在ScrollBarDrawable中,包括三个基本尺寸range/offset/extent、以及用来标识滚动条背景和自身的两个图片track/thumb;
    range:代表滚动条从头至尾滚动过程中所跨越的范围有多大,比如你想用滚动条来标示一万行代码,那range就可以设置10000;
    offset:代表滚动条当前的偏移量,或者是可视的第一行在整个滚动跨度的什么位置,比如当前已经滚动到600行了,那offset就是600;
    extent:代表显示滚动条的视图View在屏幕上的可视高度或宽度,比如200dp;
    track:代表滚动条显示的背景或轨道,一般其宽高度和extent一致;
    thumb:代表滚动条显示的具体前景,其宽高度、位置会根据range/offset/extent三者的具体值进行计算获得;


    image.png

    3)滚动条一共有三种状态ON/OFF/FADDING,即显示、隐藏、正在隐藏(处于显示状态,但通过设置透明度悄悄改变状态);
    一般而言,从用户体验的角度来说滚动条会在滚动完毕后自动隐藏,而滚动时自动显示,开发者可以通过scrollBarFadeDuration参数设置自动隐藏间隔时间,也可以调用setScrollbarFadingEnabled()设置是否自动隐藏;
    4)开发者可重载3个 computeVerticalScrollXXX()方法来实现对滚动条具体显示位置和thumb大小的控制,可以调用awakenScrollBars()方法强制显示滚动条;
    自定义滚动位置时可调用scrollTo()/scrollBy()来完成滚动到具体位置、或者滚动具体距离;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349