Activity的测量(Measure)、布局(Layout)和绘制(Draw)过程分析
一个Android应用程序窗口里面包含了很多UI元素,这些UI元素是以树形结构来组织的,即它们存在着父子关系,其中,子UI元素位于父UI元素里面,因此,在绘制一个Android应用程序窗口的UI之前,我们首先要确定它里面的各个子UI元素在父UI元素里面的大小以及位置。确定各个子UI元素在父UI元素里面的大小以及位置的过程又称为测量过程和布局过程。因此,Android应用程序窗口的UI渲染过程可以分为测量、布局和绘制三个阶段,如图1所示:
从前面Android应用程序窗口(Activity)的视图对象(View)的创建过程分析一文可以知道,Android应用程序窗口的顶层视图是一个类型为DecorView的UI元素,而从前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文的Step 3又可以知道,这个顶层视图最终是由ViewRoot类的成员函数performTraversals来启动测量、布局和绘制操作的,这三个操作分别由DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw来实现的。
接下来,我们就分别从DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw开始,分析Android应用程序窗口的测量、布局和绘制过程。
1. Android应用程序窗口的测量过程
DecorView类的成员函数measure是从父类View继承下来的,因此,我们就从View类的成员函数measure开始分析应用程序窗口的测量过程,如图2所示:
参数child用来描述当前要测量大小的子视图,参数parentWidthMeasureSpec和parentHeightMeasureSpec用来描述当前子视图可以获得的最大宽度和高度,参数widthUsed和heightUsed用来描述父窗口已经使用了的宽度和高度。ViewGroup类的成员函数measureChildWithMargins必须要综合考虑上述参数,以及当前正在测量的子视图child所设置的大小和Margin值,还有当前视图容器所设置的Padding值,来得到当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec,这是通过调用ViewGroup类的另外一个成员函数getChildMeasureSpec来实现的。
得到了当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,就可以调用它的成员函数measure来设置它的大小了,即执行前面Step 1的操作。注意,如果当前正在测量的子视图child描述的也是一个视图容器,那么它又会重复执行Step 2和Step 3(递归)的操作,直到它的所有子孙视图的大小都测量完成为止。
至此,我们就分析完成Android应用程序窗口的测量过程了,接下来我们继续分析Android应用程序窗口的布局过程。
2. Android应用程序窗口的布局过程
DecorView类的成员函数layout是从父类View继承下来的,因此,我们就从View类的成员函数layout开始分析应用程序窗口的布局过程,如图3所示:
View类的成员函数layout首先调用另外一个成员函数setFrame来设置当前视图的位置以及大小。设置完成之后,如果当前视图的大小或者位置与上次相比发生了变化,那么View类的成员函数setFrame的返回值changed就会等于true。在这种情况下, View类的成员函数layout就会继续调用另外一个成员函数onLayout重新布局当前视图的子视图。
此外,如果此时View类的成员变量mPrivateFlags的LAYOUT_REQUIRED位不等于0,那么也表示当前视图需要重新布局它的子视图,因此,这时候View类的成员函数layout也会调用另外一个成员函数onLayout。
View类的成员函数layout最后还会将成员变量mPrivateFlags的FORCE_LAYOUT位设置为0,也是因为此时当前视图及其子视图的布局已经是最新的了。
FrameLayout类的成员函数onLayout通过一个for循环来布局当前视图的每一个子视图。如果一个子视图child是可见的,那么FrameLayout类的成员函数onLayout就会根据当前视图可以用来显示子视图的区域以及它所设置的gravity属性来得到它在应用程序窗口中的左上角位置(childeLeft,childTop)。
当一个子视图child在应用程序窗口中的左上角位置确定了之后,再结合它在前面的测量过程中所确定的宽度width和高度height,我们就可以完全地确定它在应用程序窗口中的布局了,即可以调用它的成员函数layout来设置它的位置和大小了,这刚好就是前面的Step 1所执行的操作。注意,如果当前正在布局的子视图child描述的也是一个视图容器,那么它又会重复执行Step 5的操作,直到它的所有子孙视图都布局完成为止。
至此,我们就分析完成Android应用程序窗口的布局过程了,接下来我们继续分析Android应用程序窗口的绘制过程。
3. Android应用程序窗口的绘制过程
ViewRoot类的成员函数draw首先会创建一块画布,接着再在画布上绘制Android应用程序窗口的UI,最后再将画布的内容交给SurfaceFlinger服务来渲染,这个过程如图4所示:
这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。
ViewRoot类的成员函数draw的执行流程如下所示:
1. 将成员变量mSurface所描述的应用程序窗口的绘图表面保存在变量surface中,以便接下来可以通过变量surface来操作应用程序窗口的绘图表面。
2. 调用成员变量mScroller所描述的一个Scroller对象的成员函数computeScrollOffset来计算应用程序窗口是否处于正在滚动的状态中。如果是的话,那么得到的变量scrolling就会等于true,这时候调用成员变量mScroller所描述的一个Scroller对象的成员函数getCurrY就可以得到应用程序窗口在Y轴上的即时滚动位置yoff。
3. 成员变量mScrollY用来描述应用程序窗口下一次绘制时在Y轴上应该滚动到的位置,因此,如果应用程序窗口不是处于正在滚动的状态,那么它在下一次绘制时,就应该直接将它在Y轴上的即时滚动位置yoff设置为mScrollY。
4. 成员变量mCurScrollY用来描述应用程序窗口上一次绘制时在Y轴上的滚动位置,如果它的值不等变量yoff的值,那么就表示应用程序窗口在Y轴上的滚动位置发生变化了,这时候就需要将变量yoff的值保存在成员变量mCurScrollY中,并且将参数fullRedrawNeeded的设置为true,表示要重新绘制应用程序窗口的所有区域。
5. 成员变量mAttachInfo所描述的一个AttachInfo对象的成员变量mScalingRequired表示应用程序窗口是否正在请求进行大小缩放,如果是的话,那么所请求的大小缩放因子就保存在这个AttachInfo对象的另外一个成员变量mApplicationScale中。函数将这两个值保存在变量scalingRequired和appScale中,以便接下来可以使用。
6. 成员变量mDirty描述的是一个矩形区域,表示应用程序窗口的脏区域,即需要重新绘制的区域。函数将这个脏区域保存变量dirty中,以便接下来可以使用。
7. 成员变量mUseGL用来描述应用程序窗口是否直接使用OpenGL接口来绘制UI。当应用程序窗口的绘图表面的内存类型等于WindowManager.LayoutParams.MEMORY_TYPE_GPU时,那么就表示它需要使用OpenGL接口来绘制UI,以便可以利用GPU来绘制UI。当应用程序窗口需要直接使用OpenGL接口来绘制UI时,另外一个成员变量mGlCanvas就表示应用程序窗口的绘图表面所使用的画布,这块画布同样是通过OpenGL接口来创建的。
8. 当应用程序窗口需要直接使用OpenGL接口来绘制UI时,函数接下来就会将它的UI绘制在成员变量mGlCanvas所描述的一块画布上,这是通过调用成员变量mView所描述的一个类型为DecorView的顶层视图的成员函数draw来实现的。注意,在绘制之前,还需要对画布进行适当的转换:A. 设置画布在Y轴上的偏移值yoff,以便可以正确反映应用程序窗口的滚动状态;B. 如果成员变量mTranslator的值不等于null,即它指向了一个Translator对象,那么就说明应用程序窗口运行在兼容模式下,这时候就需要相应对画布进行变换,以便可以正确反映应用程序窗口的大小;C. 当变量scalingRequired的值等于true时,同样说明应用程序窗口是运行在兼容模式下,这时候就需要修改画布在兼容模式下的点密度,以便可以正确地反映应用程序窗口的分辨率,注意,这时候屏幕在兼容模式下的点密度保存在DisplayMetrics类的静态成员变量DENSITY_DEVICE中。由于上述画布的转换操作只针对当前的这一次绘制操作有效,因此,函数就需要在绘制之后,调用画布的成员函数save来保存它在转换前的矩阵变换堆栈状态,以便在绘制完成之后,可以调用画布的成员函数restoreToCount来恢复之前的矩阵变换堆栈状态。
9. 使用OpenGL接口来绘制完成UI后,如果变量scrolling的值等于true,即应用程序窗口是处于正在滚动的状态,那么就意味着应用程序窗口接下来还需要马上进行下一次重绘,而且是所有的区域都需要重绘,因此,函数接下来就会将成员变量mFullRedrawNeeded的值设置为true,并且调用另外一个成员函数scheduleTraversals来请求执行下一次的重绘操作。
10. 以下的步骤针适用于使用非OpenGL接口来绘制UI的情况,也是本文所要关注的重点。
11. 参数fullRedrawNeeded用来描述是否需要绘制应用程序窗口的所有区域。如果需要的话,那么就会将应用程序窗口的脏区域的大小设置为整个应用程序窗口的大小(0,0,mWidth,mHeight),其中,成员变量mWidth和mHeight表示应用程序窗口的宽度和高度。注意,如果应用程序窗口的大小被设置了一个缩放因子,即变量appScale的值不等于1,那么就需要将应用程序窗口的宽度mWidth和高度mHeight乘以这个缩放因子,然后才可以得到应用程序窗口的实际大小。
12. 经过前面的一系列计算之后,如果应用程序窗口的脏区域dirty不等于空,或者应用程序窗口在正处于动画状态,即成员变量mIsAnimating的值等于true,那么函数接下来就需要重新绘制应用程序窗口的UI了。在绘制之前,首先会调用用来描述应用程序窗口的绘图表面的一个Surface对象surface的成员函数lockCanvas来创建一块画布canvas。有了这块画布之后,接下来就可以调用成员变量mView所描述的一个类型为DecorView的顶层视图的成员函数draw来在上面绘制应用程序窗口的UI了。 与前面的第8步一样,在绘制之前,还需要对画布进行适当的A、B和C转换,以及需要在绘制之后恢复画布在绘制之前的矩阵变换堆栈状态。
13. 绘制完成之后,应用程序窗口的UI就都体现在前面所创建的画布canvas上了,因此,这时候就需要将它交给SurfaceFlinger服务来渲染,这是通过调用用来描述应用程序窗口的绘图表面的一个Surface对象surface的成员函数unlockCanvasAndPost来实现的。
14. 在请求SurfaceFlinger服务渲染应用程序窗口的UI之后,函数同样是需要判断变量scrolling的值是否等于true。如果等于的话,那么就与前面的第9步一样,函数需要将成员变量mFullRedrawNeeded的值设置为true,并且调用另外一个成员函数scheduleTraversals来请求执行下一次的重绘操作。
在本文中,我们只关注使用非OpenGL接口来绘制应用程序窗口的UI的步骤,其中,第12步和第13步是关键所在。第12步调用了Java层的Surface类的成员函数lockCanvas来为应用程序窗口的绘图表面创建了一块画布,并且调用了DecorView类的成员函数draw来在这块画布上绘制了应用程序窗口的UI,而第13步调用了Java层的Surface类的成员函数unlockCanvasAndPost来将前面已经绘制了应用程序窗口UI的画布交给SurfaceFlinger服务来渲染