Android——View的工作原理
Posted SyubanLiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——View的工作原理相关的知识,希望对你有一定的参考价值。
前言
要想实现自定义View,那么需掌握View的事件体系及其底层工作原理,本节主要围绕View的工作原理来开展,View的事件体系可见另一篇文章。
View的底层工作原理主要涉及到三个流程:View的测量(measure)、View的布局(layout)、View的绘制(draw)。除了这三大基本流程外,有几个View的回调方法也需掌握,如:构造方法、onAttach、onVisibilityChanged、onDetach等等。
自定义View的实现,主要有几种固定类型,有的继承View或者ViewGroup,有的直接继承现有的系统控件。
1. ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完成后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象(具体细节可去了解android启动流程),并将ViewRootImpl对象和DecorView建立关联。
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
View的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout和draw 三个过程后才能最终将一个View绘制出来。其中measure用于测量View的宽高,layout用于View在父容器的放置位置,draw用于View的绘制。
private void performTraversals() {
// 这个方法代码非常多,但是重点就是执行这三个方法
// 执行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 执行布局(ViewGroup)中才会有
performLayout(lp, mWidth, mHeight);
// 执行绘制
performDraw();
}
performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法。
Measure 过程决定了View的宽高,Measure完成以后,可以通过 getMeasureWidth 和 getMeasureHeight 方法来获取到View的测量后的宽高,在一般情况下,它都等于View的最终的宽高,但在某些特情况下除外。
Layout 过程决定了View的四个顶点的位置坐标和实际View的宽高,分别对应着View的 top、left、right、bottom 四个顶点位置,并可以通过 getWidth 和 getHeight 来获取View的实际宽高。
Draw 过程决定View的显示,只有draw完成以后,View才能呈现在屏幕上。
2. MeasureSpec(测量规范)
MeasureSpec 参与View的 measure 过程,很大程度上决定一个View的尺寸规格。在View的测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据其测量出View的宽高。
MeasureSpec 代表一个 32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量模式下的大小)。通过这种方式,可以避免过多的内存开销。一组SpecMode和SpecSize可以打包成一个MeasureSpec。
2.1 MeasureSpec基础
关于SpecMode,主要有三类:
- UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
- EXACTLY:父容器检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize指定的值。对应于LayoutParams中的 match_parent 和 具体的数值 这两种模式。
- AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。对应于LayoutParams的wrap_content。
2.2 MeasureSpec 和 LayoutParams 对应关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后根据这个MeasureSpec来确定View测量后的宽高。因此,View的 MeasureSpec 是由 View自身LayoutParams 和 父容器的MeasureSpec 一块决定的。
对于DecorView,其MeasureSpec由窗口的尺寸和自身的LayoutParams共同决定;对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身LayoutParams来共同决定。
SpecMode的规则根据LayoutParams中的宽高这样划分:
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
- 固定大小:精确模式,大小为LayoutParams中指定的大小;
在这可以简单说一下:
- 当View采用固定宽高时,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循LayoutParams中的大小;
- 当View宽高是 match_parent 时,如果父容器是精确模式,那么View也是精确模式且其大小是父容器的剩余空间;若父容器是最大模式时,那么View也是最大模式且其大小不会超过父容器的剩余空间;
- 当View的宽高时 wrap_content 时,不管父容器是哪种模式,View的模式总是最大化且大小不超过父容器的剩余空间。
3. View的工作流程
View的工作流程主要是指:measure、layout、draw,即 测量、布局和绘制。measure确定View的测量宽高,layout确定View的四个顶点位置及最终宽高,draw负责将View绘制到屏幕上。
3.1 measure过程
3.1.1 View的measure过程
对于 AT_MOST 和 EXACTLY 这两种情况, getDefaultSize 返回的大小就是 measureSpec 中的 SpecSize,而这个SpecSize就是测量后的大小。
对于 UNSPECIFIED,一般用于系统内部的测量过程,在这种情况下,有两个方法比较重要: getSuggestedMinimumWidth 与 getSuggestedMinimumHeight,这两个方法的的逻辑是:如果View没有设置背景,那么View的宽度为View的mMinWidth,这个值可以是0;若View设置了背景,那么就会通过 max(mMinWidth, mBackground.getMinimumWidth()) 去取最大值,这两方法返回值就是View在UNSPECIFIED情况下的测量宽高。
因此,直接继承View的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在不居中使用 wrap_content 相当于使用了 match_parent。解决办法就是给View指定一个默认的内部宽高并在 wrap_content 时设置此宽高接口。
3.1.2 ViewGroup的measure过程
对于ViewGroup来说,除了完成自身的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。在 measureChild 方法中,思想是去取子元素的LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给View的measure方法来进行测量。
因此,ViewGroup 的 measure 过程是先去遍历测量子元素的宽高,当子元素测量完毕后,ViewGroup再去根据子元素的情况来测量自身的宽高。
View的measure过程是三大流程中最复杂的一个,当measure完成后,可以通过 getMeasureWidth 和 getMeasureHeight 来获取测量的宽高,在一般情况下,测量的宽高等于最终的宽高,但由于 padding 或者 margin 等因素会导致不同,因此建议是在 onLayout 下去获取View的最终宽高。
问题来了,如何在Activity启动过程中去获取某个View的宽高呢?由于View的measure过程跟Activity的生命周期不是同步执行的,所以无法保证在Activity某个生命周期时,该View已经测量完成,如果没有测量完成,那么取出来的宽高就为0。那么接下来提供几种方法来获取:
- Activity/View#onWindowFocusChanged:当Activity窗口的焦点丢失或者得到时,都会调用此方法,因此会存在频繁调用问题,即Activity的onResume与onPause频繁执行时,该方法也会被频繁执行。但在这个方法中,View已经初始化完毕,可去获取View的宽高。
- view.post(runnable):通过post可以将一个runnable投递到消息队列的尾部(这里可去研究Android启动流程、VSync刷新、Handler的同步屏障),然后等待Looper调用此runnable时,View已经初始化完毕。
- ViewTreeObserver:使用 ViewTreeObserver 众多回调可以完成此操作,例如 OnGlobalLayoutListener 这个接口,当View树的状态发生改变或者View树内部的可见性发生改变时,onGlobalLayout就会被回调。但要注意的是,如果View树的状态发生改变时,该方法可以会被执行多次。
- view.measure(widthMeasureSpec, heightMeasureSpec):通过手动的方式去对View进行measure过程来得到View的宽高,需要根据View的LayouParams分情况处理。
3.2 layout 过程
layout方法确定View本身的位置,而onLayout方法会确定子元素的位置。
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定View的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom,View的四个顶点一旦确定,那么View在父容器中位置也就确定了;接着调用onLayout 方法,这个方法时父容器确定子元素的位置,其具体的实现和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout方法。
3.3 draw 过程
draw过程的流程如下:
- 绘制背景 background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
View的绘制的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法。
View还有一个特殊的方法 setWillNotDraw,如果一个View不需要绘制内容,那么会设置这个标志位为true,系统会进行相应的优化。在默认情况下,View没有启用这个优化标志位,ViewGroup会默认启用这个标志位。因此这个标志位的作用是:自定义继承于ViewGroup并且本身不需要绘制功能时,就开启该优化标志位;若明确ViewGroup需要进行绘制onDraw时,我们需要手动关闭这个 WILL_NOT_DRAW 标志位。
4. 自定义View
自定义View是一个综合体系,它涉及到 View的层次结构、事件分发机制和View的工作原理。
4.1 自定义View的分类
- 继承 View 重写 onDraw 方法
- 继承 ViewGroup 派生特殊的 Layout
- 继承特定的 View (例如TextView)
- 继承特定的 ViewGroup (例如LinearLayout)
4.2 自定义View须知
- 让 View 支持 wrap_content(设定固定宽高)
- 如有必要让 View 支持 padding:如果不在View的onDraw中处理padding的话,那么padding属性是不起作用的
- 尽量不要在 View 中使用 Handler,可使用 View 自身的post
- View 中若有 动画或者线程,需要及时停止,避免发生内存泄露,View#onDetachedFromWindow
- 当 View 带有 滑动或者嵌套 情形时,需要处理好滑动冲突
以上是关于Android——View的工作原理的主要内容,如果未能解决你的问题,请参考以下文章