1 Android窗口模型
理解View工作原理之前,有几个基本概念需要阐述清楚:
1.1 Window
Window是一个抽象类,代表的就是手机屏幕,其具体实现是PhoneWindow,Android中所有的视图都是通过Window来呈现的,无论是Activity、Dialog还是Toast,它们的视图实际上都是依附在Window上,因此Window实际上是View的直接管理者。
1.2 ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的工作原理同ViewRoot紧密相连.
DecorView是Activity的顶级View,包含标题栏和内容栏,内容栏是一定的存在的,而且其id为android.R.id.content,通过setContentView()将布局文件添加到内容栏中,
这篇文章有助于我们理解
|
|
Window,Activity,DecorView,ContentView等基本概念,通过图来说明
1.3 DecorView的创建过程
View离不开DecorView,了解DecorView的创建过程对于理解View的工作原理大有裨益。以一个简单例子来引入:
再来追踪下Activity的setContentView()方法:
接下来进入到PhoneWindow中看看DecorView是如何被创建的:
进入到installDecor()中进一步分析:
进入generateDecor()方法中:
方法很简单,就是new一个DecorView对象。但是此时创建的DecorView仅仅是一个空白的FrameLayout,接下来便是通过generateLayout()方法初始化DecorView的结构:
源码中这个方法很长,但其主要作用就是加载android.R.id.content这个布局文件,然后添加到DecorView中,并初始化mContentParent。以上便完成了DecorView结构的初始化工作。
接下来再回到setContentView()方法中:
以上过程只是完成了DecorView的创建和初始化工作,创建好了之后实际上DecovView仍旧处于INVISIBLE状态,还需要将DecorView添加到屏幕上,这时就需要用到WindowManager。分下Activity的启动过程,首先调用ActivityThread的handleResumeActivity,接着调用Activity.onResume()方法,接着调用Activity.makeVisible()方法:
以上过程便DecorView的整个创建以及初始化过程,这种见省略了不少其他的操作。那么,DecorView是如何同ViewRootImpl关联上的呢?
继续以上的分析:
WindowManager的真实实现是WindowManagerImpl,因此实际上调用的也是WindowManagerImpl.addView()方法:
接下来进入到WindowManagerGlobal.addView()方法中,这个方法比较复杂,这里作节选:
在这个方法中,创建View对应的ViewRoot对象,ViewRoot控制着一个视图的结构,里面包含于WindowManager通信的Binder对象、View所在界面的ContextImpl、DecorView等信息。
接下来进入到ViewRootImpl.setView()方法中:
接下来看一下requestLayout()方法:
再进入到scheduleTraversals()方法中:
OKAY!!终于要开始进入到View的绘制流程了!
2 View的绘制
View的绘制绘制流程是从performTraversals()方法开始的,经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的位置,draw负责将View绘制在屏幕上。其大致流程如下图所示:
上图描述了View绘制的大致流程。下面将详细分析每一步的流程:
2.1 理解MeasureSpec
理解MeasureSpec对理解View的测量过程是必须的,字面意思就是“测量说明书”,MeasureSpec从始至终一直参与者View的测量过程。
(1) MeasureSpec
MeasureSpec是一个32为的int值,高2为代表SpecMode,意思就是测量模式,低30位代表SpecSize,是指在某种测量模式下的规格大小。
从上面代码可以看出,MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为方便操作,其提供了打包和解包操作。
SpecMode有三类:
UNSPECIFIED
父容器不对View作任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View所需的精确尺寸大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现,它对应于LayoutParams中的wrap_content。
(2) MeasureSpec和LayoutParams的对应关系
正常情况下,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步确定View的宽/高。
DecorView的MeasureSpec创建过程
在RootViewImpl中的measureHierarchy方法中:
其中,childWidthMeasureSpec/childHeightMeasureSpec分别为屏幕的尺寸,不难看出,对于顶级DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。
普通View的MeasureSpec创建过程
对普通的View而言,其measure过程由ViewGroup传递而来。
先来看看ViewGroup的measureChildWidhMargins()方法:
上述代码展示了对ViewGroup中的子元素进行measure过程,在调用子元素的measure方法之前,会先通过getChildMeasureSpec()方法获取子元素的MeasureSpec,同时不难发现,子元素MeasureSpec的创建同父容器的MeasureSpec和子元素本身的LayoutParams直接相关,此外还与子元素的margin及padding有关。getChildMeasureSpec()方法则更进一步说明了子元素MeasureSpec计算过程,具体有兴趣的可以自行了解下。
至此,获取MeasureSpec过程也就结束了,这是View绘制的第一步。
2.2 View的measure过程
measure过程需分2中情况来讨论:原始的View和ViewGroup。对于原始的View,通过measure方法就完成了自身的测量过程,对于ViewGroup,除了完成自己的测量之外,还要去递归遍历调用所有子元素的measure方法。
(1) View的measure过程
承接上文,在View的measure()方法中,会调用onMeasure()方法,来看看onMeasure()方法:
可以看出,onMeasure()方法相当简单,setMeasuredDimension()顾名思义,就是设置View的宽/高,如何设置?来看下getDefaultSize()方法:
简单来看,对绝大多数情况(AT_MOST和EXACTLY),getDefaultSize()返回的就是specSize,而这个specSize就是View的测量大小。但要注意,View最终的大小是在layout阶段确定的,但是几乎在所有情况下View的测量大小总是和最终的大小是相等的。UNSPECIFIED较少用到,追踪源码其实也挺简单,这里不作讨论。
(2) ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程之外,还会递归遍历所有的子元素。ViewGroup是一个抽象类,没有重写View的onMeasure()方法,但是提供了measureChildren()的方法:
上述代码中,ViewGroup进行measure时,会对每一个子元素进行measure,再来看看measureChild()方法:
如此便最终实现了ViewGroup的测量过程!!但,归根到底,ViewGroup并没有定义测量的具体过程,这主要是因为ViewGroup是一个抽象类,其测量过程中的onMeasure()方法需要各个子类去具体的实现,比如LinearLayout、RelativeLayout等各自定义了自己的onMeasure()方法。由于ViewGroup子类的不同的布局特性,导致它们的测量细节各不相同,因此ViewGroup并没有实现onMeasure()方法,而是交给具体的子类来实现。具体的可以参考LinearLayout的onMeasure()过程,这里不作细述。
(3) 一个问题
刚入门Android的时候,特别是在做Dolphin中的一些动画时候,经常要获取某个特定View个宽/高,当时傻啦吧唧的试图在Activity的onCreate()中获取(因为View的宽/高需要比较早知道),可是无论是view.getWidth()还是view.getMeasuredWidth(),返回的都是0!无赖在onResume()中获取,发现偶尔不为0,但大多数时候都为0!!不知道有没有和我一样SHA的。
实际上,在onCreate()、onStart()、onResume()中都无法正确的获取View的宽/高信息,这是因为View的measure过程和Activity的生命周期不是同步执行的!无法保证在Activity的这些方法调用时,View的测量过程已经完成了,那么如果一定要获取View的宽/高信息呢?该如何做?下面提供几种方法:
Activity/View#onWindowFoucusChanged()方法中获取
onWindowFoucusChanged()方法说明View已经初始化完毕,宽/高信息已经准备好了,在这里获取View的宽/高信息基本是正确的。
(1) View.post(runnable)方法
(1) ViewTreeObserver方法方法
使用ViewTreeObserver的回调方法也可以完成View的测量,比如在OnGlobalLayoutListener这个接口中做。
2.3 View的Layout过程
Layout过程的作用是ViewGroup用来确定其子元素的位置,当ViewGroup位置被确定之后,它在onLayout()中会遍历所有子元素并调用其layout()方法,在layout()方法中onLayout()方法被调用,最终确定所有元素的位置。
也就是:layout()方法确定View本身的位置,而onLayout()方法确定所有子元素的位置。
先来看看View的layout()方法中的关键代码:
从上面代码可以看出,layout()方法的大致流程是:首先通过setFrame()方法来设置View的4个顶点位置,进入setFrame()方法不难看出,其实也就是设置mLeft, mRight, mTop, mBottom这四个值,View的4个顶点确定之后,View在父容器中的位置也就确定了;然后,调用onLayout()方法,这个方法的作用是父容器确定其子元素的位置,onLayout()在View中并无具体的实现,同onMeasure()方法类似,其具体实现依赖于具体的布局,因此View和ViewGroup均没有真正实现onLayout()方法,我们以LinearLayout中的onLayout()方法为例:
继续看看layoutVertical()实现:
可以看出,此方法遍历所有子元素并调用setChildFrame()方法来为子元素指定对应的位置,setChildFrame()中调用了View的layout()方法,最终又回到了上面所提到的情况。
回到前文所提到的:绝大多数情况下,View的测量宽/高和最终的宽/高是相等的,在这里也可以得到证明:
而在setFrame()方法中,
从上面的onLayout()方法中知道,
因此,View的默认实现中,View的测量宽/高和最终的宽/高是相等的。除了一下情况:
这种情况下,View的最终宽/高总是比测量宽/高大100px。另外一种情况是,View需要多次measure测能确定自己的测量宽/高,前几次的测量过程中,其测量出来的宽/高有可能和最终的宽/高不一致,但最终来讲,测量宽/高还是和最终的宽/高相同。
2.4 View的draw过程
View的draw()过程比较简单,它的作用是将View绘制在屏幕上,draw()过程分为以下几部:
(1) 绘制背景;
(2) 绘制自己;
(3) 绘制children;
(4) 绘制装饰。
来简要看下draw()方法的实现:
View绘制过程的传递是通过dispatchDraw()完成的,dispatchDraw()会遍历调用所有子元素的draw()方法,如此实现draw事件的层层传递。
3 References
[1] Android源码:http://grepcode.com/search/?query=google+android&entity=project,本文所有代码基于Android 5.1.1_r1
[2] 《Android开发艺术探索》