View绘制

控件树的创建、测量、布局、绘制、销毁

Posted by candy1126xx on April 10, 2017

重要的类

  • PhoneWindow:自定义内容与DecorView、WMS之间的桥梁。
  • Callback/OnWindowDismissedCallback:WMS回调。
  • mWindowManager(WindowManagerImpl):向WMS发送信息的装饰层对象。
  • mDecor(DecorView):控件树的根节点。
  • mContentRoot(ViewGroup):DecorView的唯一子结点,根据mWindowStyle的不同解析不同xml文件生成。
  • mContentParent(ViewGroup):mContentRoot中ID为ID_ANDROID_CONTENT的ViewGroup;自定义内容的根节点。
  • mWindowAttributes(WindowManager.LayoutParams):窗口参数;随PhoneWindow创建默认值。
  • WindowManagerGlobal:进程单例;应用进程向WMS发送信息的客户端Session.Proxy的装饰类。
  • mViews:应用进程中所有窗口的DecorView集合。
  • mRoots:应用进程中所有窗口的ViewRootImpl集合
  • mParams:应用进程中所有窗口的WindowManager.LayoutParams集合。
  • 以上3者以索引一一对应。
  • ViewRootImpl:WMS与View框架之间协议的实现类;WindowManagerGlobal的细节实现;封装了一棵控件树的信息。
  • mWindow(ViewRootImpl.W):窗口唯一标示;其他组件向ViewRootImpl发送信息的服务端。
  • mView(DecorView):控件树的根节点。
  • mWindowAttributes:
  • mAttachInfo:View所属窗口信息;随ViewRootImpl创建;控件树中所有View都持有它的引用。
  • mWinFrame;和WMS中窗口的位置尺寸是同步的,在ViewRootImpl.W的回调中被更新。
  • mWidth/mHeight:DecorView的尺寸,不是随WMS实时更新的;每次relayout时从mWinFrame读取数据更新。
  • mDirty:脏区域。
  • mLayoutRequested:在发送测布绘消息前置为true;在执行测布绘时作为进入“控件树布局”的条件之一;在进入“控件树布局”之后置为false。
  • mStoped:所属窗口是否处于“停止”状态;在Activity.onStop()后置为true;在Activity.onResume()后置为false;作为进入“控件树布局”的条件之一;
  • mLayoutRequesters:当某控件调用requestLayout(),便会把该控件的引用加入mLayoutRequesters;
  • mSuface:当前窗口的Surface。
  • Surface:代表保存在Native的一个”raw buffer”,”raw buffer”的内容是屏幕上的一块画面;并提供了操作”raw buffer”的方法。
  • mLockedObject:保存了Native层”raw buffer”的地址。
  • View.AttachInfo:View所属窗口信息。
  • mWindowLeft/mWindowTop:View所属窗口的位置,不是随WMS实时更新的;每次relayout时从mWinFrame读取数据更新;是View的绝对坐标的原点。
  • mGivenInternalInsets:
  • mHardwareAccelerated/mHardwareAccelerationRequest:这两者决定是否启用“硬件绘制”。
  • mHardwareRenderer:
  • ThreadedRender:硬件绘制的工具类。
  • mRootNodeNeedsUpdate:当需要绘制控件树时置为true;作为真正开始绘制(updateRootDisplayList())的条件之一。
  • mRootNode:“作用相当于”复制当前处理的DecorView的RenderNode,粘贴给Native层的“绘制执行者”。
  • mSurfaceWidth/mSurfaceHeight:当前操作的Surface的尺寸;在DecorView再测量后,取DecorView.mWidth/DecorView.mHeight。
  • mInsetTop/mInsetLeft:当前绘制操作的坐标系原点;在DecorView再测量后,取ViewRootImpl.mWindowAttributes.surfaceInsets。
  • Choreographer:Android刷新机制在Java层的实现者。
  • View:
  • mRenderNode:只有根View的才有用;由它创建的DisplayListCanvas是这棵控件树的所有绘制操作的记录者。
  • mLayerPaint:
  • RenderNode:对应一个Native层的“绘制节点”;封装了所属控件的引用,和Native层的“一组绘制动作”的地址。
  • mNativeRenderNode:Native层“一组绘制动作”的地址。
  • start():创建一个DisplayListCanvas,并设置Viewport;意味着开始记录“绘制操作”。
  • end():通知Native层绘制结束,并回收DisplayListCanvas;意味着结束记录并开始执行“绘制操作”。
  • DisplayListCanvas:对应一个RenderNode;Canvas的硬件绘制实现,把“绘制动作”记录在对应的RenderNode.mNativeRenderNode;使用了对象池技术。

控件树结构实现

  • View.mParent(ViewParent):该控件的父节点;
  • ViewGroup.mChildren(View[]):该控件的所有子节点;

创建控件树

分为两个过程,一是DecorView加入WMS,二是自定义内容接到DecorView。这两个过程不分先后。

DecorView加入WMS

在Activity.handleResumeActivity()中:

  1. Activity生命周期onNewIntents() -> onActivityResult() -> onResume();
  2. 获取(有则返回没有创建)Activity中的PhoneWindow;
  3. 获取(有则返回没有创建)PhoneWindow中的DecorView;
  4. 获取(有则返回没有创建)PhoneWindow中的WindowManager;
  5. 获取(有则返回没有创建)PhoneWindow中的WindowManager.LayoutParams;
  6. 把WindowManager.LayoutParams的type设置为TYPE_BASE_APPLICATION;
  7. 构造ViewRootImpl;
  8. 把WindowManager.LayoutParams设置给DecorView;
  9. 把DecorView加入WindowManager.mViews;
  10. 把ViewRootImpl加入WindowManager.mRoots;
  11. 把WindowManager.LayoutParams加入WindowManager.mParams;
  12. 用DecorView、WindowManager.LayoutParams初始化ViewRootImpl的成员变量;
  13. 发送测布绘消息;
  14. 向WMS发送信息“用参数mWindowAttributes,向mDisplay添加一个窗口,窗口唯一标示为mWindow,窗口所属组件标示为mWindowAttributes.token”;
  15. 进行第一次完整测布绘。

自定义内容接到DecorView

在Activity.setContainView() -> PhoneWindow.setContainView()中:

  1. 如果DecorView为null,构造DecorView;
  2. 根据mWindowStyle的不同解析不同xml文件生成mContentRoot;
  3. 找到mContentRoot中ID为ID_ANDROID_CONTENT的mContentParent;
  4. 把自定义内容加入mContentParent。

测布绘的驱动力

Choreographer

测量控件树

测量控件树分为三步:自测量窗口resize再测量。测量后,控件树中每个控件(包括DecorView)的最终尺寸被赋值到View.measuredWidth/View.measuredHeight。

自测量

  1. 确定DecorView的指导尺寸。如果是第一次测布绘,应用进程和WMS进程都没有窗口尺寸,如果是系统窗口,取DisplayInfo.logical;如果不是,取DisplayMatrix;如果不是第一次测布绘,应用进程和WMS进程都有上次协商的窗口尺寸,取这个尺寸;
  2. 开始后根遍历控件树。
  3. 第一步确定子控件的指导尺寸。
  4. 如果父控件的指导尺寸是“EXACTLY”,
    • 子控件的mLayoutParams是精确数据,那么子控件的指导尺寸是“mLayoutParams的数据,EXACTLY”;
    • 子是“MATCH“,那么子的指导尺寸是“父的数据 - padding,EXACTLY”;
    • 子是“WRAP“,那么子的指导尺寸是“父的数据 - padding,AT_MOST”;
  5. 如果父控件的指导尺寸是“AT_MOST”,
    • 子控件的mLayoutParams是精确数据,那么子控件的指导尺寸是“mLayoutParams的数据,EXACTLY”;
    • 子是“MATCH“,那么子的指导尺寸是“父的数据 - padding,AT_MOST”;
    • 子是“WRAP“,那么子的指导尺寸是“父的数据 - padding,AT_MOST”;
  6. 如果父控件的指导尺寸是“UNSPECIFIED”,
    • 子控件的mLayoutParams是精确数据,那么子控件的指导尺寸是“mLayoutParams的数据,EXACTLY”;
    • 子是“MATCH“,那么子的指导尺寸是“父的数据 - padding,UNSPECIFIED”;
    • 子是“WRAP“,那么子的指导尺寸是“父的数据 - padding,UNSPECIFIED”;
  7. 第二步,如果子控件有PFLAG_FORCE_LAYOUT标记且可见,
    • 如果子控件是ViewGroup,递归3;
    • 如果子控件是View,计算尺寸并设置到measuredWidth/measuredHeight。默认的计算方法是,如果指导尺寸是“AT_MOST”或“EXACTLY”,则计算结果为指导尺寸的数据部分;否则为mMinWidth/mMinHeight。View的子类会重写onMeasure()以实现自己特定的计算方法。
  8. 第三步,根据子控件的计算结果、父控件的指导尺寸,计算父控件的尺寸。ViewGroup没有默认实现方法,子类必须重写onMeasure()以实现自己特定的计算方法。

整个遍历过程总结为:先从根开始进行广度优先遍历,计算所有控件的指导尺寸;再从叶子结点回溯,计算所有控件的准确尺寸。

窗口resize

进入条件

满足以下任意一条。

  • 第一次测布绘;
  • DecorView完成自测量;&&(窗口尺寸与协商尺寸不同;   指导尺寸是WRAP;   自测量尺寸与协商尺寸不同)
  • WMS进程中Insets与应用进程不同;
  • DecorView可见性发生变化;
  • mWindowAttributes有变化;
  • 软键盘有变化;
过程
  1. 向WMS发送信息“唯一标示为mWindow的窗口请求重新布局,参数为params,宽高为DecorView自测量尺寸”;
  2. 在WMS中,根据mWindow找到WindowState;
  3. 根据params和DecorView自测量尺寸修改WindowState的宽高、可见性等;
  4. 全局窗口排序布局(performLayoutAndPlaceSurfacesLocked());
  5. 处理Native层Surface;
  6. 向应用进程发送信息“窗口的位置和尺寸改变了”;
  7. 在应用进程中,更新ViewRootImpl.mWinFrame;

再测量

进入条件

窗口resize后,ViewRootImpl.mWidth/mHeight与ViewRootImpl.mWinFrame不同

过程
  1. 根据ViewRootImpl.mWinFrame更新ViewRootImpl.mWidth/mHeight、View.AttachInfo.mWindowLeft/mWindowTop;
  2. 以ViewRootImpl.mWidth/mHeight为DecorView的指导尺寸,再次后根遍历控件树(与自测量相同)。

布局控件树

布局后,控件树中每个控件(包括DecorView)的mLeft、mTop、mRight、mBottom被赋值。

进入条件

ViewRootImpl.mLayoutRequested为true,且所属窗口不处于“停止”状态。

过程
  1. 先根遍历控件树;
  2. 第一步,设置父控件自己的mLeft、mTop、mRight、mBottom。DecorView的取值是0,0,measuredWidth,measuredHeight;
  3. 第二步,在onLayout()中计算并设置子控件的mLeft、mTop、mRight、mBottom。ViewGroup没有默认实现,子类必须重写onLayout()以实现自己特定的计算方法。
  4. 第三步,如果子控件是ViewGroup,递归3。

绘制控件树(Java层)

硬件绘制

  1. mRootNodeNeedsUpdate置为true;
  2. 重置mDirty;
  3. 创建一个DisplayListCanvas;
  4. DisplayListCanvas记录:设置Viewport为DecorView.mWidth/DecorView.mHeight;
  5. DisplayListCanvas记录:坐标系对齐到ViewRootImpl.mWindowAttributes.surfaceInsets;
  6. DisplayListCanvas记录:Reorder分界线;
  7. 先根遍历控件树,依次调用View.draw(),参数是DecorView的RenderNode创建的DisplayListCanvas;最终控件树中所有绘制操作记录在DecorView的RenderNode下;
  8. DisplayListCanvas记录:绘制操作是DecorView的RenderNode记录的“一组绘制操作”;
  9. DisplayListCanvas记录:Inorder分界线;
  10. 通知Native层绘制结束,并回收DisplayListCanvas。

软件绘制

  1. 为mDirty区域创建一个”raw buffer”,并返回操作”raw buffer”的接口Canvas;
  2. Canvas操作:设置屏幕分辨率;
  3. Canvas操作:清除原来的数据;
  4. 重置mDirty;
  5. Canvas操作:坐标系对齐到ViewRootImpl.mWindowAttributes.surfaceInsets;
  6. 先根遍历控件树,依次调用View.draw();最终”raw buffer”就是屏幕上应该显示的内容;
  7. 把”raw buffer”复制到帧缓存,并释放”raw buffer”。

一些小问题

对透明区域的处理方法

先假设整个窗口是透明的;在布局DecorView之后,遍历所有控件,如果控件没有PFLAG_SKIP_DRAW标记,就把它从透明区域中减去,否则,把View的背景、前景中不透明的部分减去。背景/前景是Drawable类,getTransparentRegion()可以获得它的透明区域。

滑动列表是否会引起测布绘

不会引起列表控件及其父控件的测量、布局,会引起列表控件的绘制、和其内子控件的布局。因为改变mScroll实际上是在改变ViewPort。

requestLayout()的作用

从此View回溯到DecorView,mPrivateFlags加上PFLAG_FORCE_LAYOUT、PFLAG_INVALIDATED;调用ViewRootImpl的requestLayout(),发送测布绘消息。

invalidate()的作用

为此View的mPrivateFlags加上PFLAG_DIRTY;回溯到DevorView,调用invalidateChildInParent(),最终把此View加入ViewRootImpl的mDirty;发送测布绘消息。当测布绘时会进入绘制阶段。

invalidate()和postInvalidate()的区别

  • 在UI线程中可以使用invalidate(),也可以使用postInvalidate();
  • 在子线程中必须使用postInvalidate();postInvalidate()实质上是利用Handler机制向UI线程发送一个Runnable,执行体中调用了invalidate()。

View排序

绘制过程中会依次遍历ViewGroup.mChildren,并比较子View的Z值。因此Z值越大越在上面;Z值相同的,在ViewGroup.mChildren中索引越大越在上面。