Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析(下)

时间:2022-08-29 21:57:21

       ViewRoot类的成员函数invalidateChild首先调用另外一个成员函数checkThread来检查当前正在执行的是否是一个UI线程。如果不是的话,ViewRoot类的成员函数checkThread就会抛出一个异常出来。这是因为所有的UI操作都必须要在UI线程中执行。

       ViewRoot类的成员函数invalidateChild接下来还会检查当前正在处理的应用程序窗口在Y轴上是否出现有滚动条,即成员变量mCurScrollY的值不等于0, 或者前正在处理的应用程序窗口是否运行在兼容模式之下,即成员变量mTranslator的值不等于null。当一个应用程序窗口运行在兼容模式时,它显示出来的大小和它实际被设置的大小是不一样的,要经过相应的转换处理。对于上述这两种情况,ViewRoot类的成员函数invalidateChild都需要调整参数dirty所描述的一个需要重新绘制的区域的大小和位置。

       调整好参数dirty所描述的一个需要重新绘制的区域之后, ViewRoot类的成员函数invalidateChild就将它所描述的一个区域与成员变量mDirty所描述的一区域执行一个合并操作,并且将得到的新区域保存在成员变量mDirty中。从这个操作就可以看出,ViewRoot类的成员变量mDirty描述的就是当前正在处理的应用程序窗口下一次所要重新绘制的总区域。

       设置好当前正在处理的应用程序窗口下一次所要重新绘制的总区域之后,ViewRoot类的成员函数invalidateChild最后就检查成员变量mWillDrawSoon的值是否不等于true。如果ViewRoot类的成员mWillDrawSoon的值等于true的话,那么就说明UI线程的消息队列中已经有一个DO_TRAVERSAL消息在等待执行了,这时候就不需要调用ViewRoot类的成员函数scheduleTraversals来向UI线程的消息队列发送一个DO_TRAVERSAL消息了,否则的话,就需要调用ViewRoot类的成员函数scheduleTraversals来向UI线程的消息队列发送一个DO_TRAVERSAL消息。

       ViewRoot类的成员函数scheduleTraversals在前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文中已经分析过了,这里不再详述。

       这一步执行完成之后,返回到前面的Step 1中,即View类的成员函数layout中,接下来它就会调用另外一个成员函数onLayout来重新布局当前视图的子视图的布局了。View类的成员函数onLayout是由子类来重写的,并且只有当该子类描述的是一个容器视图时,它才会重写父类View的成员函数onLayout。前面我们已经假设当前正在处理的是应用程序窗口的顶层视图,它的类型为DecorView,并且它描述的是一个容器视图,因此,接下来我们就会继续分析DecorView类的成员函数onLayout的实现。

       事实上,DecorView类是通过FrameLayout类来间接继承View类的,并且它的成员函数onLayout是从FrameLayout类继承下来的,因此,接下来我们实际上要分析的是FrameLayout类的成员函数onLayout的实现。

       Step 5. FrameLayout.onLayout

publicclassFrameLayout extendsViewGroup {
......
protectedvoidonLayout(booleanchanged, intleft, inttop, intright, intbottom) {
finalintcount = getChildCount();
finalintparentLeft = mPaddingLeft + mForegroundPaddingLeft;
finalintparentRight = right - left - mPaddingRight - mForegroundPaddingRight;
finalintparentTop = mPaddingTop + mForegroundPaddingTop;
finalintparentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;
mForegroundBoundsChanged = true;
for(inti = 0; i < count; i++) {
finalView child = getChildAt(i);
if(child.getVisibility() != GONE) {
finalLayoutParams lp = (LayoutParams) child.getLayoutParams();
finalintwidth = child.getMeasuredWidth();
finalintheight = child.getMeasuredHeight();
intchildLeft = parentLeft;
intchildTop = parentTop;
finalintgravity = lp.gravity;
if(gravity != -1) {
finalinthorizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
finalintverticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch(horizontalGravity) {
caseGravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
break;
caseGravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2+
lp.leftMargin - lp.rightMargin;
break;
caseGravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
break;
default:
childLeft = parentLeft + lp.leftMargin;
}
switch(verticalGravity) {
caseGravity.TOP:
childTop = parentTop + lp.topMargin;
break;
caseGravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2+
lp.topMargin - lp.bottomMargin;
break;
caseGravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
......
}

       这个函数定义在文件frameworks/base/core/java/android/widget/FrameLayout.java中。


       FrameLayout类的成员变量mPaddingLeft、mPaddingRight、mPaddingTop、mPaddingBottom和mForegroundPaddingLeft、mForegroundPaddingRight、mForegroundPaddingTop、mForegroundPaddingBottom的含义我们在前面分析Android应用程序窗品的测量过程时已经解释过了,它们描述的是当前视图的内边距,而参数left、top、right和bottom描述的是当前视图的外边距,即它与父窗口的边距。通过上述这些参数,我们就可以得到当前视图的子视图所能布局在的区域。

       FrameLayout类的成员函数onLayout通过一个for循环来布局当前视图的每一个子视图。如果一个子视图child是可见的,那么FrameLayout类的成员函数onLayout就会根据当前视图可以用来显示子视图的区域以及它所设置的gravity属性来得到它在应用程序窗口中的左上角位置(childeLeft,childTop)。

       当一个子视图child在应用程序窗口中的左上角位置确定了之后,再结合它在前面的测量过程中所确定的宽度width和高度height,我们就可以完全地确定它在应用程序窗口中的布局了,即可以调用它的成员函数layout来设置它的位置和大小了,这刚好就是前面的Step 1所执行的操作。注意,如果当前正在布局的子视图child描述的也是一个视图容器,那么它又会重复执行Step 5的操作,直到它的所有子孙视图都布局完成为止。

      至此,我们就分析完成Android应用程序窗口的布局过程了,接下来我们继续分析Android应用程序窗口的绘制过程。

      ViewRoot类的成员函数draw首先会创建一块画布,接着再在画布上绘制Android应用程序窗口的UI,最后再将画布的内容交给SurfaceFlinger服务来渲染,这个过程如图4所示:

图4 Android应用程序窗口的绘制过程

       Step 1. ViewRoot.draw

publicfinalclassViewRoot extendsHandler implementsViewParent,View.AttachInfo.Callbacks {......privatevoiddraw(booleanfullRedrawNeeded) {Surface surface = mSurface;......intyoff;finalbooleanscrolling = mScroller != null&& mScroller.computeScrollOffset();if(scrolling) {yoff = mScroller.getCurrY();} else{yoff = mScrollY;}if(mCurScrollY != yoff) {mCurScrollY = yoff;fullRedrawNeeded = true;}floatappScale = mAttachInfo.mApplicationScale;booleanscalingRequired = mAttachInfo.mScalingRequired;Rect dirty = mDirty;......if(mUseGL) {if(!dirty.isEmpty()) {Canvas canvas = mGlCanvas;if(mGL != null&& canvas != null) {......intsaveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);try{canvas.translate(0, -yoff);if(mTranslator != null) {mTranslator.translateCanvas(canvas);}canvas.setScreenDensity(scalingRequired? DisplayMetrics.DENSITY_DEVICE : 0);mView.draw(canvas);......} finally{canvas.restoreToCount(saveCount);}......}}if(scrolling) {mFullRedrawNeeded = true;scheduleTraversals();}return;}if(fullRedrawNeeded) {......dirty.union(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));}......if(!dirty.isEmpty() || mIsAnimating) {Canvas canvas;try{......canvas = surface.lockCanvas(dirty);......} catch(Surface.OutOfResourcesException e) {......return;} catch(IllegalArgumentException e) {......return;}try{if(!dirty.isEmpty() || mIsAnimating) {.....mView.mPrivateFlags |= View.DRAWN;......intsaveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);try{canvas.translate(0, -yoff);if(mTranslator != null) {mTranslator.translateCanvas(canvas);}canvas.setScreenDensity(scalingRequired? DisplayMetrics.DENSITY_DEVICE : 0);mView.draw(canvas);} finally{......canvas.restoreToCount(saveCount);}......}} finally{surface.unlockCanvasAndPost(canvas);}}......if(scrolling) {mFullRedrawNeeded = true;scheduleTraversals();}}......}

       这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。


       1. 将成员变量mSurface所描述的应用程序窗口的绘图表面保存在变量surface中,以便接下来可以通过变量surface来操作应用程序窗口的绘图表面。

       3. 成员变量mScrollY用来描述应用程序窗口下一次绘制时在Y轴上应该滚动到的位置,因此,如果应用程序窗口不是处于正在滚动的状态,那么它在下一次绘制时,就应该直接将它在Y轴上的即时滚动位置yoff设置为mScrollY。

       5. 成员变量mAttachInfo所描述的一个AttachInfo对象的成员变量mScalingRequired表示应用程序窗口是否正在请求进行大小缩放,如果是的话,那么所请求的大小缩放因子就保存在这个AttachInfo对象的另外一个成员变量mApplicationScale中。函数将这两个值保存在变量scalingRequired和appScale中,以便接下来可以使用。

       7. 成员变量mUseGL用来描述应用程序窗口是否直接使用OpenGL接口来绘制UI。当应用程序窗口的绘图表面的内存类型等于WindowManager.LayoutParams.MEMORY_TYPE_GPU时,那么就表示它需要使用OpenGL接口来绘制UI,以便可以利用GPU来绘制UI。当应用程序窗口需要直接使用OpenGL接口来绘制UI时,另外一个成员变量mGlCanvas就表示应用程序窗口的绘图表面所使用的画布,这块画布同样是通过OpenGL接口来创建的。

      9. 使用OpenGL接口来绘制完成UI后,如果变量scrolling的值等于true,即应用程序窗口是处于正在滚动的状态,那么就意味着应用程序窗口接下来还需要马上进行下一次重绘,而且是所有的区域都需要重绘,因此,函数接下来就会将成员变量mFullRedrawNeeded的值设置为true,并且调用另外一个成员函数scheduleTraversals来请求执行下一次的重绘操作。

     11. 参数fullRedrawNeeded用来描述是否需要绘制应用程序窗口的所有区域。如果需要的话,那么就会将应用程序窗口的脏区域的大小设置为整个应用程序窗口的大小(0,0,mWidth,mHeight),其中,成员变量mWidth和mHeight表示应用程序窗口的宽度和高度。注意,如果应用程序窗口的大小被设置了一个缩放因子,即变量appScale的值不等于1,那么就需要将应用程序窗口的宽度mWidth和高度mHeight乘以这个缩放因子,然后才可以得到应用程序窗口的实际大小。

     13. 绘制完成之后,应用程序窗口的UI就都体现在前面所创建的画布canvas上了,因此,这时候就需要将它交给SurfaceFlinger服务来渲染,这是通过调用用来描述应用程序窗口的绘图表面的一个Surface对象surface的成员函数unlockCanvasAndPost来实现的。

      在本文中,我们只关注使用非OpenGL接口来绘制应用程序窗口的UI的步骤,其中,第12步和第13步是关键所在。第12步调用了Java层的Surface类的成员函数lockCanvas来为应用程序窗口的绘图表面创建了一块画布,并且调用了DecorView类的成员函数draw来在这块画布上绘制了应用程序窗口的UI,而第13步调用了Java层的Surface类的成员函数unlockCanvasAndPost来将前面已经绘制了应用程序窗口UI的画布交给SurfaceFlinger服务来渲染。接下来,我们就分别分析Java层的Surface类的成员函数lockCanvas、DecorView类的成员函数draw和Java层的Surface类的成员函数unlockCanvasAndPost的实现。

Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析一文,这里不再详述。

      至此,我们就分析完成Android应用程序窗口的渲染过程了,从中就可以看出:

      1. 渲染Android应用程序窗口UI需要经过三步曲:测量、布局、绘制。

      2. Android应用程序窗口UI首先是使用Skia图形库API来绘制在一块画布上,实际地是绘制在这块画布里面的一个图形缓冲区中,这个图形缓冲区最终会被交给SurfaceFlinger服务,而SurfaceFlinger服务再使用OpenGL图形库API来将这个图形缓冲区渲染到硬件帧缓冲区中。

      Android应用程序窗口的渲染过程分析完成之后,Android应用程序窗口的实现框架就分析完成了,重新学习请回到Android应用程序窗口(Activity)实现框架简要介绍和学习计划一文中。

      在Android应用程序窗口(Activity)实现框架简要介绍和学习计划这一系列文章中,我们主要是从单个应用程序窗口的角度来分析的。但是,Android系统在运行的过程中,需要管理的是一系列的应用程序窗口,并且这些应用程序窗口的类型可能各不相同,并且相互影响。因此,Android的窗口管理系统是非常复杂的。在接下来的一个系列的文章中,我们就将详细地分析Android窗口管理服务WindowManagerService的实现,以便可以从系统的角度来分析应用程序窗口的实现。敬请关注!

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!