3.View的绘制流程

View是在什么时候显示在屏幕上面的?(如:MainActivity的布局文件activity_main.xml)

  • setContentView最终的结果是将解析的xml文件中的View添加到DecorView中.

那么这个DecorView是什么时候添加到Window(PhoneWindow)的呢?

  • DecorView是在ActivityThread.java的handleResumeActivity()方法中,performResumeActivity()方法后面添加到PhoneWindow中的,具体的添加代码如下:
    wm.addView(decor, l);
    参数分析:
    wmWindowManagerImpl,为什么不是ViewManager呢?
    因为在中途设置了windowManager的值为WindowManagerImpl,代码如下:
    //Activity.java中的attach方法里
    //setWindowManager方法中将一个创建的WindowManagerImpl赋值给了WindowManager
    mWindow.setWindowManager(...);
    mWindowManager = mWindow.getWindowManager();
    
    //Window.java中实现上面setWindowManager(...)方法的代码实现
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) {
        //省略部分代码        
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
    
    decorDeorView,lWindowManager.LayoutParams

总结: 也就是说,View显示在屏幕上,其实是在onResume生命周期方法后面,通过WindowManagerDecorView显示在屏幕上的.

View被显示到屏幕上Window的过程?

  • 调用wm.addView(decor, l);方法去添加decorView;
  • 而创建的wm其实是WindowManagerImpl的实例,而WindowManagerImpl这个类中的addView方法;
  • 调用的mGlobal.addView方法,其实是调用的WindowManagerGlobal中的addView方法
  • 调用WindowManagerGlobal中的addView方法后,接着就会调用到ViewRootImpl中的addView方法

总结:
DecorView展示到屏幕PhoneWindow上,实际上是调用的WindowManagerGlobal.java中的addView方法,然而实际的调度是分配给一个个的ViewRootImpl去完成setView方法

分析WindowManagerGlobal.java中的addView方法

  • 创建ViewRootImpl对象实例root
  • 将数据缓存到集合,数据指DecorViewViewRootImplWindowManager.LayoutParams
    • mViews.add(view); //这的mViews集合中缓存的是DecorView
    • mRoots.add(root);//这的mRoots集合中缓存的是ViewRootImpl
    • mParams.add(wparams);//这的mParams集合中缓存的是WindowManager.LayoutParams
  • 调用root.setView(view, wparams, panelParentView, userId);方法

过程中涉及到三个类:

  • WindowManangerImpl
    • 确定View属于哪个屏幕/父窗口
  • WindowMangerGlobal
    • 管理整个App进程的所有的窗口信息,也就是说一个进程对应一个WindowManagerGlobal
  • ViewRootImpl
    • WindowManagerGlobal的实际操作类,操作对应的窗口
    • ViewRootImpl构造函数中的一些变量分析
      • mThread = Thread.currentThread(); 存储创建View的线程,一般是在主线程中创建View
      • mDirty = new Rect(); 脏数据,存储如TextView文字发生改变的信息
      • mAttachInfo = new View.AttachInfo(…) 保存当前窗口的一些信息

分析ViewRootImpl中的setView方法

  • 调用requestLayout()方法请求遍历,里面的流程如下
    • scheduleTraversals();
    • mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    • doTraversal();
    • performTraversals(); 这个方法中就是在绘制View
  • 调用mWindowSession.addToDisplayAsUser
    • 将窗口添加到WMS上面
  • 调用view.assignParent(this);
    • 分配父容器,通过view.getParent方法可以拿到根ViewRootImpl实例root

分析ViewRootImpl.java绘制View的方法performTraversals()

  • measureHierarchy() 预测量,里面最多可执行3次测量操作
  • relayoutWindow() 布局窗口,在WMS中处理mWindowSession.relayout
  • performMeasure() 控件树测量
  • performLayout() 布局
  • performDraw() 绘制

分析measureHierarchy()方法中的3次预测量performMeasure()

  • 期望的窗体宽度desiredWindowWidth大于baseSize进行第一次测量
  • 测量的状态太小,MEASURED_STATE_TOO_SMALL值为1时,调整baseSize大小后进行第二次测量,调整方式如下:
    baseSize = (baseSize+desiredWindowWidth)/2;
  • 如果goodMeasure值为false,进行第三次测量

总结:
所以View的绘制流程中最多可能涉及到4次测量,3次预测量;如果在预测量后,窗体大小可能还会发生变化,windowSizeMayChangetrue时,还需1次测量

setContentViewLayoutInflaterinflat方法,root参数为null时,布局文件中的根控件rootView的宽高属性失效问题?
代码举例:
LayoutInflater.from(this).inflate(R.layout.merge_layout,null,false);
因为在root参数为null的时候,inflat()方法源码中,不会对资源文件.xml的根控件设置LayoutParam,也就是说布局文件最外层控件没有LayoutParam值,所以我们布局文件中的根控件的宽高是不起作用的,从而导致了上面的问题.

面试题:UI刷新只能在主线程中进行吗?
不是的
因为在View绘制的过程中,会调用checkThread()方法,检查创建创建ViewRootImpl的线程和当前线程是否为同一个线程,如果创建ViewRootImpl的线程和当前线程不是同一个线程(创建ViewRootImpl的线程,存放在ViewRootImpl中的变量是mThread),则会报如下错误:
"Only the original thread that created a view hierarchy can touch its views."
如何在子线程中刷新UI呢?

  1. ViewRootImpl还没有创建的时候去刷新UI,这个时候就不会调用ViewRootImpl里面的checkThread方法
  2. 在子线程中创建ViewRootImpl,然后就可以在子线程中刷新UI了;创建WindowManager,然后将我们的布局文件的ViewWindowManager.LayoutParam设置进去,调用WindowManager.addView方法,就可以刷新UI了.

View绘制流程中几个方法分析

  • onMeasure
    • 作用:测量到控件的宽高
    • 流程:ViewRootImpl.performMeasure()->(DecorView)mView.measure()->View.measure()->onMeasure
    • 重点:MeasureSpec测量模式,高2位是测量模式getMode(),低30位是测量的值getSize();测量模式包括了:UNSPECIFIED(wrap_content)、EXACTLY(100dp)和AT_MOST(match_parent)
    • 扩展:View在测量的时候需要加上自己的padding,而ViewGroup在测量的时候需要加上自己的margin
  • onLayout
    • 作用:确定测量的控件布局在屏幕上的坐标位置(left、top、right和bottom),到了这一步onLayout我们才能在Activity中获取到View的宽和高,通过view.getMeasureHeight
    • 流程:ViewRootImpl.performLayout()->(DecorView)host.layout->View.layout()->onLayout
  • onDraw
    • 流程:ViewRootImpl.performDraw()->draw()
      • ->scrollToRectOrFocus()作用举例:输入框获取焦点时,页面整体往上滚动达到输入法在输入框下面的目的
      • ->硬件加速绘制 mAttachInfo.mThreadedRenderer.draw() 硬件加速绘制效果会更好
      • ->软件绘制 drawSoftware()
      • 流程:(DecorView)mView.draw()->View.draw()->View.onDraw()//绘制当前控件
      • ->View.dispatchDraw()//绘制当前控件的子控件

ViewRootImpl流程图:
3.View的绘制流程_第1张图片

ViewGroup为什么不执行onDraw()方法?

  • View.draw(canvas); 这里的View是DecorView,这个方法中会同时执行onDraw和dispatchDraw方法
    • -> onDraw(canvas); 注意:这个地方执行的是一个参数的onDraw方法
    • -> dispatchDraw(canvas); 接下来我们来分析这个方法
  • ViewGrpup.dispatchDraw(Canvas canvas) 注意:ViewGroup中只有dispatchDraw方法,没有onDraw方法
    • -> ViewGrpup.drawChild()
    • -> child.draw(canvas, this, drawingTime);注意:ViewGroup.java中调用的是View中的三个参数的draw方法
    • -> View.draw(Canvas canvas, ViewGroup parent, long drawingTime)
    • -> updateDisplayListIfDirty();分析这个方法中执行dispatchDraw和draw方法的逻辑,默认情况下会执行if逻辑判断的代码,从而导致了ViewGroup中不会执行draw方法而只执行dispatchDraw方法
      if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
          //默认情况下进入这个逻辑判断,执行dispatchDraw方法,这是一个递归的过程,会一层一层去遍历去绘制子View
          dispatchDraw(canvas);
          if (mOverlay != null && !mOverlay.isEmpty()) {
              mOverlay.getOverlayView().draw(canvas);
          }
      } else {
          //这里draw方法中就会执行onDraw和dispatchDraw方法,但是默认情况下不会走到这个逻辑判断中来
          draw(canvas);
      }
      

总结:
ViewGroup之所以不会执行onDraw方法,是因为源码中只有dispatchDraw方法,查看该方法的代码逻辑,默认他会走dispatchDraw方法逻辑,而不会走draw方法逻辑(这个方法会同时执行onDraw和dispatch方法),所以ViewGroup不会执行onDraw方法.

你可能感兴趣的:(Android,UI,Android,View绘制流程)