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
流程图:
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方法.