View是在什么时候显示在屏幕上面的?(如:MainActivity的布局文件activity_main.xml)
setContentView最终的结果是将解析的xml文件中的View添加到DecorView中.
那么这个DecorView是什么时候添加到Window(PhoneWindow)的呢?
DecorView是在ActivityThread.java的handleResumeActivity()方法中,performResumeActivity()方法后面添加到PhoneWindow中的,具体的添加代码如下:
wm.addView(decor, l);
参数分析:
wm是WindowManagerImpl,为什么不是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); }decor是DeorView,l是WindowManager.LayoutParams
总结: 也就是说,
View显示在屏幕上,其实是在onResume生命周期方法后面,通过WindowManager将DecorView显示在屏幕上的.
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 - 将数据缓存到集合,数据指
DecorView、ViewRootImpl和WindowManager.LayoutParams- mViews.add(view); //这的mViews集合中缓存的是
DecorView - mRoots.add(root);//这的mRoots集合中缓存的是
ViewRootImpl - mParams.add(wparams);//这的mParams集合中缓存的是
WindowManager.LayoutParams
- mViews.add(view); //这的mViews集合中缓存的是
- 调用
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次预测量;如果在预测量后,窗体大小可能还会发生变化,windowSizeMayChange为true时,还需1次测量
接setContentView中LayoutInflater的inflat方法,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呢?
- 在
ViewRootImpl还没有创建的时候去刷新UI,这个时候就不会调用ViewRootImpl里面的checkThread方法 - 在子线程中创建
ViewRootImpl,然后就可以在子线程中刷新UI了;创建WindowManager,然后将我们的布局文件的View和WindowManager.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
- 作用:确定测量的控件布局在屏幕上的坐标位置(left、top、right和bottom),
- onDraw
- 流程:ViewRootImpl.performDraw()->draw()
- ->scrollToRectOrFocus()
作用举例:输入框获取焦点时,页面整体往上滚动达到输入法在输入框下面的目的 - ->硬件加速绘制 mAttachInfo.mThreadedRenderer.draw() 硬件加速绘制效果会更好
- ->软件绘制 drawSoftware()
- 流程:(DecorView)mView.draw()->View.draw()->View.onDraw()//绘制当前控件
- ->View.dispatchDraw()//绘制当前控件的子控件
- ->scrollToRectOrFocus()
- 流程:ViewRootImpl.performDraw()->draw()
ViewRootImpl流程图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSsNvpaD-1684050356871)(evernotecid://3A22127D-0425-400F-9B33-054BC6B3328D/appyinxiangcom/40619816/ENResource/p19)]](https://img-blog.csdnimg.cn/6d9f1fd57f444e2c97cb1cfbed6ab2de.png)
ViewGroup为什么不执行onDraw()方法?
- View.draw(canvas);
这里的View是DecorView,这个方法中会同时执行onDraw和dispatchDraw方法- -> onDraw(canvas);
注意:这个地方执行的是一个参数的onDraw方法 - -> dispatchDraw(canvas);
接下来我们来分析这个方法
- -> onDraw(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方法.



















