View是如何显示出来的
- 调用Activity的attach方法,该方法会创建PhoneWindow对象
- 在onCreate的setContentView方法中,先会通过PhoneWindow的getDecorView方法获取DecorView,如果没有则创建DecorView对象
- 获取DecorView后,创建subDecor,里面包括标题和内容两部分,其中内容设置id为android.R.id.content。DecorView将添加subDecor
- 通过LayoutInflater方法加载我们设置的xml布局,移除全部id为android.R.id.content的ViewGroup中的所有子View,最后调用addView添加我们的View
- 在 handleResumeActivity 方法调用完 activity 的 onResume 方法后,会通过PhoneWindow获取WindowManagerImpl来调用addView方法,其内部会调用WindowManagerGlobal.addView方法,最后调到ViewRootImpl的 setView 方法。
- 在setView方法中调用 requestLayout 方法,其内部先会检查线程,然后调用scheduleTraversals方法
- scheduleTraversals方法是通过Handle实现,目的是确保每一次绘制都在vsync脉冲信号发出时调用 doTraversal 方法
- doTraversal 方法会调用 performTraversals,先调用DecorView的dispatchAttachedToWindow方法,分发onAttachedToWindow 事件;然后执行 measure、layout、draw 流程
- 在setView方法中将当前PhoneWindow添加到WindowManagerService(WMS)上
了解上面的流程,一些常见的面试题就很容易回答了。常见面试题如下:
面试题1:在onResume中是否可以获取View的宽高
第一次启动activity时不能在onResume中获取。根据上面View的显示流程,view的measure是在onResume方法后执行的。如果是从其他的activity回到当前activity而执行的onResume方法,那么就能够获取到View的宽高
面试题2:为什么子线程不能更新UI?
可以在子线程更新UI。根据上面view的显示流程,Android是在ViewRootImpl的requestLayout方法中检查线程的,如果在ViewRootImpl创建之前更新UI是可以的。之所以google要求在主线程更新UI,是因为如果允许多线程更新UI,但是UI的接口没有加锁的,一旦多线程抢占了资源,那么界面更新将会混乱。如果加了锁则会影响提高界面的流畅性。
面试题3:Activity,Window,View三者的联系和区别
-
Activity是安卓四大组件之一,负责界面、交互和业务逻辑处理。其内部包含一个PhoneWindow,负责处理View的相关逻辑
-
Window目前实现类只有PhoneWindow,Window是View的载体,负责管理View
-
View则是放在Window容器的元素
面试题4:View的绘制流程是从Activity的哪个生命周期方法开始执行的
根据上面view的显示流程,可以知道view的绘制流程是在activity的onResume方法后执行的
View的绘制流程
- View的绘制从 ViewRootImpl 类的 performTraversals() 方法开始
- 调用 measureHierarchy 方法开始预测量阶段
- 在预测量阶段,会根据WindowManager.LayoutParams和屏幕宽高来确定DecorView的MeasureSpec。其中WindowManager.LayoutParams的width、height默认为MATCH_PARENT;当为弹窗时,才会设置为WRAP_CONTENT
- 执行 performMeasure 方法开始预测量,内部调用 DecorView 的 measure,然后调用它的 onMeasure 方法,由于DecorView是FrameLayout,其内部会先调用 measureChildWidthMargins 方法,它会根据当前View的MeasureSpec和子View的LayoutParams(xml解析时被创建)来计算出子View的MeasureSpec。如果是child是View,则会先后调用它的measure、onMeasure,最后调用setMeasureDimension 方法保存自己的尺寸;如果child是ViewGroup,则先测量子View的尺寸,最后调用setMeasureDimension 方法保存自己的尺寸。
- 测量阶段,重复步骤4
- 调用 performLayout 方法,内部调用 DecorView 的 layout方法,内部先调用 setFrame 方法设置自己的大小和位置,然后调用 onLayout 方法,其内部调用子View的layout方法完成布局。
- 调用 performDraw 方法,内部调用 DecorView 的 draw方法,draw方法内部会先调drawBackground(Canvas)方法绘制背景;后调用onDraw(Canvas)绘制主体;再调用 dispatchDraw(Canvas) 方法绘制子View;最后调用 onDrawForeground(Canvas) 方法绘制滑动条和前景。
面试题5:View绘制流程中,onMeasure方法将执行几次
最少执行2次,最多执行4次。如果界面为弹窗,那么在activity的setContentView方法中会设置WindowManager.LayoutParams的width为WRAP_CONTENT,这时在预测量阶段会有协商的过程,这时最多会测量3次。默认情况下预测量阶段只会测量一次。因此onMeasure方法最少执行2次,最多执行4次。
面试题6:getMeasuredWidth和getWidth的区别
getMeasureWidth方法实现是返回 MeasureSpec的大小,因此它在setMeasureDemention后设置了MeasureSpec后才有值;而getWidth()实现是(mRight - mLeft),它是在layout中setFrame()方法中赋值。
面试题7:ViewGroup的onDraw什么情况下执行
ViewGroup的onDraw()方法默认是不执行的,若要ViewGroup的onDraw()执行,只需要setWillNotDraw(false)、设置背景、设置前景、设置焦点高亮,4个选项其中一项满足即可。
面试题8:说一说你对MeasureSpec的理解
MeasureSpec 代表一个 32 位 int 值,高 2 位代表测量模式 SpecMode,低 30 位代表规格大小 SpecSize,MeasureSpec 通过把 SpecMode 和 SpecSize 打包成一个 int 值避免过多的对象内存分配
SpecMode有三种模式
- UNSPECIFIED:不指定测量模式, 父视图没有限制子视图的大小,子视图可以是任何想要的尺寸,通常用于系统内部,应用开发中很少用到
- EXACTLY:精确测量模式,对应于match_parent或具体数值,这种模式下View的测量值就是SpecSize的值
- AT_MOST:最大值测量模式,对于wrap_content,这种模式下子View的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。
对于普通的View,MeasureSpec的计算规则如下。它的MeasureSpec是由父视图的MeasureSpec和其自身的LayoutParams共同决定的。特殊的是,对于DecorView而言,它的MeasureSpec由屏幕宽高和WindowManger.LayoutParams来决定的。
面试题9:invalidate、postInvalidate、requestLayout 的区别
- invalidate():在UI线程调用。view的invalidate会导致当前view被重绘,由于mLayoutRequested为false,不会导致onMeasure和onLayout被调用,而OnDraw会被调用
- postInvalidate():可以在子线程调用,内部是通过 ViewRootImpl 的handler切换到UI线程,最终执行 invalidate()
- requestLayout():view的requestLayout会直接递归调用父窗口的requestLayout,直到ViewRootImpl,然后触发peformTraversals,由于mLayoutRequested为true,会导致onMeasure和onLayout被调用,不一定会触发OnDraw;requestLayout触发onDraw可能是因为在在layout过程中发现l,t,r,b和以前不一样,那就会触发一次invalidate,所以触发了onDraw