深入理解ViewRootImpl
深入理解ViewRootImpl
这部分内容来自 《 深入理解ViewRoot 》。
ViewRootImpl实现了ViewParent接口,作为整个控件树的根部,它是控件树正常运作的动力所在,控件的测量、布局、绘制以及输入事件的派发处理都由ViewRootImpl触发。另一方面,它是WindowManagerGlobal工作的实际实现者,因此它还需要负责与WMS交互通信以调整窗口的位置大小,以及对来自WMS的事件(如窗口尺寸改变等)作出相应的处理。
1 ViewRootImpl的创建及其几个重要的成员
ViewRootImpl创建于WindowManagerGlobal的addView()方法中,而调用addView()方法的线程即是此ViewRootImpl所掌控的控件树的UI线程。
(1)ViewRootImpl#构造函数:
public ViewRootImpl(Context context, Displaydisplay) {
/* ① 从WindowManagerGlobal中获取一个IWindowSession的实例。它是ViewRootImpl和
WMS进行通信的代理 */
mWindowSession= WindowManagerGlobal.getWindowSession(context.getMainLooper());
// **②保存参数display**,在后面setView()调用中将会把窗口添加到这个Display上
mDisplay= display;
CompatibilityInfoHolder cih = display.getCompatibilityInfo();
mCompatibilityInfo = cih != null ? cih : new CompatibilityInfoHolder();
/* **③ 保存当前线程到mThread。**这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主线程。
在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检
查发起请求的thread与这个mThread是否相同。倘若不同则会拒绝这个请求并抛出一个异常*/
mThread= Thread.currentThread();
......
/* **④ mDirty用于收集窗口中的无效区域。**所谓无效区域是指由于数据或状态发生改变时而需要进行重绘
的区域。举例说明,当应用程序修改了一个TextView的文字时,TextView会将自己的区域标记为无效
区域,并通过invalidate()方法将这块区域收集到这里的mDirty中。当下次绘制时,TextView便
可以将新的文字绘制在这块区域上 */
mDirty =new Rect();
mTempRect = new Rect();
mVisRect= new Rect();
/* **⑤ mWinFrame,描述了当前窗口的位置和尺寸。**与WMS中WindowState.mFrame保持着一致 */
mWinFrame = new Rect();
/* ⑥ 创建一个W类型的实例,W是IWindow.Stub的子类。即它将在WMS中作为新窗口的ID,以及接
收来自WMS的回调*/
mWindow= new W(this);
......
/* **⑦ 创建mAttachInfo。**mAttachInfo是控件系统中很重要的对象。它存储了此当前控件树所以贴附
的窗口的各种有用的信息,并且会派发给控件树中的每一个控件。这些控件会将这个对象保存在自己的
mAttachInfo变量中。mAttachInfo中所保存的信息有WindowSession,窗口的实例(即mWindow),
ViewRootImpl实例,窗口所属的Display,窗口的Surface以及窗口在屏幕上的位置等等。所以,当
要需在一个View中查询与当前窗口相关的信息时,非常值得在mAttachInfo中搜索一下 */
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display,this, mHandler, this);
/* **⑧ 创建FallbackEventHandler。**这个类如同PhoneWindowManger一样定义在android.policy
包中,其实现为PhoneFallbackEventHandler。FallbackEventHandler是一个处理未经任何人
消费的输入事件的场所。在6.5.4节中将会介绍它 */
mFallbackEventHandler =PolicyManager.makeNewFallbackEventHandler(context);
......
/* ⑨ 创建一个依附于当前线程,即主线程的Choreographer,用于通过VSYNC特性安排重绘行为 */
mChoreographer= Choreographer.getInstance();
......
}
说明:Choreographer是用来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI操作,它 接收显示系统的时间脉冲(垂直同步信号-VSync信号),每接受一个 VSync信号,就要执行上面的 三个UI操作,暂时知道这么多就可以了,有兴趣的可以参加这篇文章《 Choreographer》 了解更多。
在构造函数之外,还有另外两个重要的成员被直接初始化:
(2)mHandler
//初始化
final ViewRootHandler mHandler = new ViewRootHandler();
final class ViewRootHandler extends Handler {
@Override
public String getMessageName(Message message) {
// 设置message名称
switch (message.what) {
case MSG_INVALIDATE://message无效
return "MSG_INVALIDATE";
........
case MSG_WINDOW_MOVED://移除 window
return "MSG_WINDOW_MOVED";
case MSG_SYNTHESIZE_INPUT_EVENT: // 系统输入事件
return "MSG_SYNTHESIZE_INPUT_EVENT";
........
}
return super.getMessageName(message);
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
if (msg.what == MSG_REQUEST_KEYBOARD_SHORTCUTS && msg.obj == null) {
// Debugging for b/27963013
throw new NullPointerException(
"Attempted to call MSG_REQUEST_KEYBOARD_SHORTCUTS with null receiver:");
}
return super.sendMessageAtTime(msg, uptimeMillis);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//处理各种Message
.....
}
}
}
说明:mHandler,类型为ViewRootHandler,一个依附于创建ViewRootImpl的线程,即主线程上的,用于将某些必须主线程进行的操作安排在主线程中执行。mHandler与mChoreographer的同时存在看似有些重复,其实它们拥有明确不同的分工与意义。由于mChoreographer处理消息时具有VSYNC特性,因此它主要用于处理与重绘相关的操作。但是由于mChoreographer需要等待VSYNC的垂直同步事件来触发对下一条消息的处理,因此它处理消息的及时性稍逊于mHandler。而mHandler的作用,则是为了将发生在其他线程中的事件安排在主线程上执行。所谓发生在其他线程中的事件是指来自于WMS,由继承自IWindow.Stub的mWindow引发的回调。由于mWindow是一个Binder对象的Bn端,因此这些回调发生在Binder的线程池中。而这些回调会影响到控件系统的重新测量、布局与绘制,因此需要此Handler将回调安排到主线程中。 说明 mHandler与mThread两个成员都是为了单线程模型而存在的。Android的UI操作不是线程安全的,而且很多操作也是建立在单线程的假设之上(如scheduleTraversals())。采用单线程模型的目的是降低系统的复杂度,并且降低锁的开销。
(2)mSurface
final Surface mSurface = new Surface();
Surface对应了一块 屏幕缓冲区 ,每个window对应一个Surface,任何View都是画在Surface上的,传统的view共享一块屏幕缓冲区,所有的绘制必须在UI线程中进行,在这里对 Surface了解这么多就可以了,想了解更多参见 《 浅谈Android Surface机制 》 。
采用无参构造函数创建的一个Surface实例。mSurface此时是一个没有任何内容的空壳子,在 WMS通过relayoutWindow()为其分配一块Surface之前尚不能实用。从这也知道了 Surface是在ViewRootImpl进行初始化的。
(3)mWinFrame、mPendingContentInset、mPendingVisibleInset以及mWidth,mHeight
// These are accessed by multiple threads.
final Rect mWinFrame; // frame given by window manager.
final Rect mPendingOverscanInsets = new Rect();
final Rect mPendingVisibleInsets = new Rect();
final Rect mPendingStableInsets = new Rect();
final Rect mPendingContentInsets = new Rect();
final Rect mPendingOutsets = new Rect();
final Rect mPendingBackDropFrame = new Rect();
int mWidth;
int mHeight;
这几个成员存储了窗口布局相关的信息。其中mWinFrame、mPendingConentInsets、mPendingVisibleInsets与窗口在WMS中的Frame、ContentInsets、VisibleInsets是保持同步的。这是因为这3个成员不仅会作为 relayoutWindow()的传出参数,而且ViewRootImpl在收到来自WMS的回调IWindow.Stub.resized()时,立即更新这3个成员的取值。因此这3个成员体现了窗口在WMS中的最新状态。与mWinFrame中的记录窗口在WMS中的尺寸不同的是,mWidth/mHeight记录了窗口在ViewRootImpl中的尺寸,二者在绝大多数情况下是相同的。当窗口在WMS中被重新布局而导致尺寸发生变化时,mWinFrame会首先被IWindow.Stub.resized()回调更新,此时mWinFrame便会与mWidth/mHeight产生差异。此时ViewRootImpl即可得知需要对控件树进行重新布局以适应新的窗口变化。在布局完成后,mWidth/mHeight会被赋值为mWinFrame中所保存的宽和高,二者重新统一。在随后分析performTraversals()方法时,读者将会看到这一处理。另外,与mWidth/mHeight类似,ViewRootImpl也保存了窗口的位置信息Left/Top以及ContentInsets/VisibleInsets供控件树查询,不过这四项信息被保存在了mAttachInfo中。接下来我们看看源码怎么实现更新的:
初始化:
mWidth = -1;
mHeight = -1;
重新布局mWidth/mHeight会被赋值为mWinFrame中所保存的宽和高:
private void performTraversals() {
......
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
......
}
IWindow.Stub#resized:
@Override
public void resized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
Configuration newConfig, Rect backDropFrame, boolean forceLayout,
boolean alwaysConsumeNavBar) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
viewAncestor.dispatchResized(frame, overscanInsets, contentInsets,
visibleInsets, stableInsets, outsets, reportDraw, newConfig, backDropFrame,
forceLayout, alwaysConsumeNavBar);
}
}
ViewRootImpl#dispatchResized:
public void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
Configuration newConfig, Rect backDropFrame, boolean forceLayout,
boolean alwaysConsumeNavBar) {
......
Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED);
......
SomeArgs args = SomeArgs.obtain();
final boolean sameProcessCall = (Binder.getCallingPid() == android.os.Process.myPid());
args.arg1 = sameProcessCall ? new Rect(frame) : frame;
args.arg2 = sameProcessCall ? new Rect(contentInsets) : contentInsets;
args.arg3 = sameProcessCall ? new Rect(visibleInsets) : visibleInsets;
args.arg4 = sameProcessCall && newConfig != null ? new Configuration(newConfig) : newConfig;
args.arg5 = sameProcessCall ? new Rect(overscanInsets) : overscanInsets;
args.arg6 = sameProcessCall ? new Rect(stableInsets) : stableInsets;
args.arg7 = sameProcessCall ? new Rect(outsets) : outsets;
args.arg8 = sameProcessCall ? new Rect(backDropFrame) : backDropFrame;
args.argi1 = forceLayout ? 1 : 0;
args.argi2 = alwaysConsumeNavBar ? 1 : 0;
msg.obj = args;
mHandler.sendMessage(msg);
}
看来是通过Message把要更新的数据给mHandler处理。
更新mWinFrame、mPendingConentInsets、mPendingVisibleInsets:
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
.....
case MSG_RESIZED: {
// Recycled in the fall through...
SomeArgs args = (SomeArgs) msg.obj;
if (mWinFrame.equals(args.arg1)
&& mPendingOverscanInsets.equals(args.arg5)
&& mPendingContentInsets.equals(args.arg2)
&& mPendingStableInsets.equals(args.arg6)
&& mPendingVisibleInsets.equals(args.arg3)
&& mPendingOutsets.equals(args.arg7)
&& mPendingBackDropFrame.equals(args.arg8)
&& args.arg4 == null
&& args.argi1 == 0) {
break;
}
} // fall through...
case MSG_RESIZED_REPORT:
if (mAdded) {
SomeArgs args = (SomeArgs) msg.obj;
Configuration config = (Configuration) args.arg4;
if (config != null) {
updateConfiguration(config, false);
}
final boolean framesChanged = !mWinFrame.equals(args.arg1)
|| !mPendingOverscanInsets.equals(args.arg5)
|| !mPendingContentInsets.equals(args.arg2)
|| !mPendingStableInsets.equals(args.arg6)
|| !mPendingVisibleInsets.equals(args.arg3)
|| !mPendingOutsets.equals(args.arg7);
mWinFrame.set((Rect) args.arg1);
mPendingOverscanInsets.set((Rect) args.arg5);
mPendingContentInsets.set((Rect) args.arg2);
mPendingStableInsets.set((Rect) args.arg6);
mPendingVisibleInsets.set((Rect) args.arg3);
mPendingOutsets.set((Rect) args.arg7);
mPendingBackDropFrame.set((Rect) args.arg8);
mForceNextWindowRelayout = args.argi1 != 0;
mPendingAlwaysConsumeNavBar = args.argi2 != 0;
args.recycle();
if (msg.what == MSG_RESIZED_REPORT) {
mReportNextDraw = true;
}
if (mView != null && framesChanged) {
forceLayout(mView);
}
requestLayout();
}
break;
.....
}
可以看出更新mWinFrame、mPendingConentInsets、mPendingVisibleInsets最终是在上面提到mHandler的handleMessage方法中进行的。
2 ViewRootImpl的setVIew()方法
ViewRootImpl的在其构造函数中初始化了一系列的成员变量,然而其创建过程仍未完成。仅在为其指定了一个控件树进行管理,并向WMS添加了一个新的窗口之后,ViewRootImpl承上启下的角色才算完全确立下来。因此需要进一步分析ViewRootImpl.setView()方法。
ViewRootImpl#setVIew:
public void setView(View view,WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
// **① mView保存了控件树的根**
mView = view;
......
// ②mWindowAttributes保存了窗口所对应的LayoutParams
mWindowAttributes.copyFrom(attrs);
......
/* 在添加窗口之前,先通过requestLayout()方法在主线程上安排一次“遍历”。所谓
“遍历”是指ViewRootImpl中的核心方法performTraversals()。这个方法实现了对
控件树进行测量、布局、向WMS申请修改窗口属性以及重绘的所有工作。由于此“遍历”
操作对于初次遍历做了一些特殊处理,而来自WMS通过mWindow发生的回调会导致一些属性
发生变化,如窗口的尺寸、Insets以及窗口焦点等,从而有可能使得初次“遍历”的现场遭
到破坏。因此,需要在添加窗口之前,先发送一个“遍历”消息到主线程。
在主线程中向主线程的Handler发送消息如果使用得当,可以产生很精妙的效果。例如本例
中可以实现如下的执行顺序:添加窗口->初次遍历->处理来自WMS的回调 */
requestLayout();
/***③ 初始化mInputChannel。**参考第五章,InputChannel是窗口接受来自InputDispatcher
的输入事件的管道。 注意,仅当窗口的属性inputFeatures不含有
INPUT_FEATURE_NO_INPUT_CHANNEL时才会创建InputChannel,否则mInputChannel
为空,从而导致此窗口无法接受任何输入事件 */
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
try {
......
/* 将窗口添加到WMS中。完成这个操作之后,mWindow已经被添加到指定的Display中去
而且mInputChannel(如果不为空)已经准备好接受事件了。只是由于这个窗口没有进行
过relayout(),因此它还没有有效的Surface可以进行绘制 */
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
} catch (RemoteException e) {......} finally { ...... }
......
if (res < WindowManagerGlobal.ADD_OKAY) {
// 错误处理。窗口添加失败的原因通常是权限问题,重复添加,或者tokeen无效
}
......
/*④ 如果mInputChannel不为空,则创建mInputEventReceiver,用于接受输入事件。
注意第二个参数传递的是Looper.myLooper(),即mInputEventReceiver将在主线程上
触发输入事件的读取与onInputEvent()。这是应用程序可以在onTouch()等事件响应中
直接进行UI操作等根本原因。
*/
if (mInputChannel != null) {
......
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
/* ViewRootImpl将作为参数view的parent。所以,ViewRootImpl可以从控件树中任何一个
控件开始,通过回溯getParent()的方法得到 */
view.assignParent(this);
......
}
}
}
至此,ViewRootImpl所有重要的成员都已经初始化完毕,新的窗口也已经添加到WMS中。ViewRootImpl的创建过程是由构造函数和setView()方法两个环节构成的。其中构造函数主要进行成员的初始化,setView()则是创建窗口、建立输入事件接收机制的场所。同时,触发第一次“遍历”操作的消息已经发送给主线程,在随后的第一次“遍历”完成后,ViewRootImpl将会完成对控件树的第一次测量、布局,并从WMS获取窗口的Surface以进行控件树的初次绘制工作。
3 ViewRootImpl中的重要成员进行了分类整理:
(1)WMS相关
- mWindowSession
- mWindow
- mDisplay
- mWindowAttributes
(2)输入事件相关
- mInputChannel
- mInputReceive
- mFallbackEventHandle
(3)控制树相关
- mVIew
- mSurface
- mAttachedInfo
(4)线程安全相关
- mHandle
- mThread
- mChoreographer
(5)布局相关
- mWinFrame
- mPendingConentInsets
- mPendingVisibleInsets
这个参数已经解释过了,就是上面黄色背景的参数。
二 Window测量
1 控件系统的心跳:performTraversals()
在上面的 ViewRootImpl#setVIew方法中调用了requestLayout()方法先对控件进行遍历, 先看几个源码(注意红色的部分):
ViewRootImpl# requestLayout:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
ViewRootImpl#scheduleTraversals:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
ViewRootImpl.TraversalRunnable:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
ViewRootImpl#doTraversal:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
这次遍历是对控件的第一次遍历,而遍历的核心方法就是 performTraversals, ViewRootImpl中接收到的各种变化,如来自WMS的窗口属性变化,来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用,并在其中完成处理。View类及其子类中的onMeasure()、onLayout()以及onDraw()等回调也都是在performTraversals()的执行过程中直接或间接地引发。 也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊地工作着,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此,performTraversals()函数可谓是ViewRootImpl的心跳。
2 performTraversals的工作阶段
performTraversals()是Android 源码中最庞大的方法之一,因此在正式探讨它的实现之前最好先将其划分为以下几个工作阶段作为指导:
(1)预测量阶段
这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次测量。测量结果可以通过mView. getMeasuredWidth()/Height()获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即窗口所期望的尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次得到回调。
(2)布局窗口阶段
根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。
(3)最终测量阶段
预测量的结果是控件树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多(以后补充,可以参见这篇《深入理解WindowManagerService》),WMS不一定会将窗口准确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。
(4)布局控件树阶段
完成最终测量之后便可以对控件树进行布局了。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。
(5)绘制阶段
这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。
3 预测量及测量原理
(1)预测量参数的候选
我们都知道View及其子类的测量工作是交给onMeasure的方法进行完成的,如下 View#onMeasure源码 :
/**
* 这个方法需要被重写,应该由子类去决定测量的宽高值,
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure需要两个参数 widthMeasureSpec, heightMeasureSpec ,这个两个参数都是MeasureSpec类型,它们的值有两个东西确定,一个是size(SPEC_SIZE,尺寸大小)另一个是mode( SPEC_MODE,模式 ) , SPEC_SIZE的初始值就是父控件给的建议尺寸 ,而View的测量结果也会收到 SPEC_MODE影响。想了解更多参见《 MeasureSpec类分析 》。
ViewRootImpl# performTraversals:
1 | private void performTraversals() { |
从上述代码可知,预测量时,SPEC_SIZE 按照如下规则进行取值:
a.第一次遍历,使用应用的最大值(一般是屏幕的宽度)作为SPEC_SIZE的候选(值)。
b.如果是悬浮窗口,其属性LayoutParam.width/height之一设置为 WRAP_CONTENT,那么将会 使用应用的最大值(一般是屏幕的宽度)作为SPEC_SIZE的候选(值)。
c.在其他的情况下,使用窗口的最新尺寸(这个值一般存储在mWinFrame中 )作为 SPEC_SIZE的候选(值)。
(2)丑陋和优雅的布局
在上面已经讲过悬浮窗口,如果其属性LayoutParam.width/height之一设置为WRAP_CONTENT,那么将会使用应用的最大尺寸(一般是屏幕的宽度)作为SPEC_SIZE的候选(值)。这里以AlertDialog为例,当其属性LayoutParam.width为WRAP_CONTENT,假设它要显示一个比较长的消息,它的宽度比较大,可能就显示一行,如下左图,看起来很丑陋,特别是在横屏模式下;那么要是能够对AlertDialog的宽度作出限制,迫使它换行,如下右图,就看起来美观多了。当然在实际应用确实存在这样的方法,实现这个功能,这个方法就是 ViewRootImpl# measureHierarchy,它需要两个至关重要的参数 desiredWindowWidth, desiredWindowHeight,这两个参数根据不同的情况进行精心挑选的, measureHierarchy将对这两个参数进行测量,尽可能的得到能够使布局优雅显示的窗口尺寸。
(3) 测量协商
接下来我们看看方法 ViewRootImpl# measureHierarchy :
private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth,
final int desiredWindowHeight) {
int childWidthMeasureSpec; // 合成后的用于描述子控件宽度的MeasureSpec
int childHeightMeasureSpec; // 合成后的用于描述子控件高度的MeasureSpec
boolean windowSizeMayChange = false; // 表示测量结果是否可能导致窗口的尺寸发生变化
boolean goodMeasure = false; // goodMeasure表示了测量是否能满足控件树充分显示内容的要求
// 测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下
if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
/* **① 第一次协商。**measureHierarchy()使用它最期望的宽度限制进行测量。这一宽度限制定义为
一个系统资源。可以在frameworks/base/core/res/res/values/config.xml找到它的定义 */
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true);
int baseSize = 0;
// 宽度限制被存放在baseSize中
if(mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if(baseSize != 0 && desiredWindowWidth > baseSize) {
// 使用getRootMeasureSpec()函数组合SPEC_MODE与SPEC_SIZE为一个MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec =
getRootMeasureSpec(desiredWindowHeight,lp.height);
//**②第一次测量。**由performMeasure()方法完成
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
/* 控件树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。如果
控件树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位 */
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)
==0) {
goodMeasure = true; // 控件树对测量结果满意,测量完成
} else {
// **③ 第二次协商。**上次测量结果表明控件树认为measureHierarchy()给予的宽度太小,
在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制 */
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
// **④ 第二次测量**
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 再次检查控件树是否满足此次测量
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)
== 0) {
goodMeasure = true; // 控件树对测量结果满意,测量完成
}
}
}
}
if(!goodMeasure) {
/* **⑤ 最终测量。**当控件树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制
做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没
有更多的空间供其使用了 */
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
/* 最后,如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口
尺寸的调整 */
if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
{
windowSizeMayChange = true;
}
}
// 返回窗口尺寸是否可能需要发生变化
return windowSizeMayChange;
}
从上面的代码不难看出, measureHierarchy测量控件树的过程就是 measureHierarchy协商的过程, measureHierarchy首先会对窗口期望的尺寸进行测量,View检查测量结果,如果对测量结果满意(测量后得到的尺寸能够满足显示内容的需要),则停止测量;如果对测量结果不满意, measureHierarchy就放宽条件 ( 使用最大宽度与期望宽度的中间值作为尺寸限制 ) ,然后依次再进行测量、检查。 倘若仍不能满足则再度进行让步,当然这个让步是有限制的只能进行两步,因为屏幕的宽度是有限的。
当 然,对于非悬浮窗口,即当LayoutParams.width被设置为MATCH_PARENT时,不存在协商过程,直接使用给定的desiredWindowWidth/Height进行测量即可。而对于悬浮窗口进行了二次协商三次测量。
(4)测量原理
测量的方法如何调用的?由谁发起的呢??
ViewRootImpl#preform Traversals——> ViewRootImpl# preformMeasure,
ViewRootImpl# preformMeasure源码如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {// mView 是父控件
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
从上面的代码可知,测量工作是由父控件调起的。
调用VIew#measure,源码如下:
public final void measure(int widthMeasureSpec,int heightMeasureSpec) {
/* 仅当给予的MeasureSpec发生变化,或要求强制重新布局时,才会进行测量。
所谓强制重新布局,是指当控件树中的一个子控件的内容发生变化时,需要进行重新的测量和布局的情况
在这种情况下,这个子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定与上次测量
时的值相同,因而导致从ViewRootImpl到这个控件的路径上的父控件的measure()方法无法得到执行
进而导致子控件无法重新测量其尺寸或布局。因此,当子控件因内容发生变化时,从子控件沿着控件树回溯
到ViewRootImpl,并依次调用沿途父控件的requestLayout()方法,在这个方法中,会在
mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利
执行,进而这个子控件有机会进行重新测量与布局。这便是强制重新布局的意义 */
if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
/* **① 准备工作。**从mPrivateFlags中将PFLAG_MEASURED_DIMENSION_SET标记去除。
PFLAG_MEASURED_DIMENSION_SET标记用于检查控件在onMeasure()方法中是否通过
调用setMeasuredDimension()将测量结果存储下来 */
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
......
/* **② 对本控件进行测量** 每个View子类都需要重载这个方法以便正确地对自身进行测量。
View类的onMeasure()方法仅仅根据背景Drawable或style中设置的最小尺寸作为
测量结果*/
onMeasure(widthMeasureSpec, heightMeasureSpec);
/* ③ 检查onMeasure()的实现是否调用了setMeasuredDimension()
setMeasuredDimension()会将PFLAG_MEASURED_DIMENSION_SET标记重新加入
mPrivateFlags中。之所以做这样的检查,是由于onMeasure()的实现可能由开发者完成,
而在Android看来,开发者是不可信的 */
if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET)
!=PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException(......);
}
// ④ 将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags。这一操作会对随后的布局操作放行
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// 记录父控件给予的MeasureSpec,用以检查之后的测量操作是否有必要进行
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
注意上面有个定义强制重新布局: 是指当控件树中的一个子控件的内容发生变化时,需要进行重新的测量和布局的情况,在这种情况下,这个子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定与上次测量时的值相同,因而导致从ViewRootImpl到这个控件的路径上的父控件的measure()方法无法得到执行进而导致子控件无法重新测量其尺寸或布局。因此,当子控件因内容发生变化时,从子控件沿着控件树回溯到ViewRootImpl,并依次调用沿途父控件的requestLayout()方法,在这个方法中,会在mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利执行,进而这个子控件有机会进行重新测量与布局。
从这段代码可以看出,View.measure()方法没有实现任何测量算法,它的作用在于引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。另外,在控件系统看来,一旦控件执行了测量操作,那么随后必须进行布局操作,因此在完成测量之后,将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags,以便View.layout()方法可以顺利进行。
接着看View#onMeasure:
/**
* 这个方法需要被重写,应该由子类去决定测量的宽高值,
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getDefaultSize???看下源码:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
View的模式为MeasureSpec.UNSPECIFIED,返回建议尺寸,否则返回specSize。
从上面可知View#onMeasure中调用了View# setMeasuredDimension,接下来我们看看这个方法:
protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) {
// ① 测量结果被分别保存在成员变量mMeasuredWidth与mMeasuredHeight中
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// ② 向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
这个方法很简单,只有两个作用:
a.保存测量结果,测量结果是通过 g etMeasuredWidthAndState()与getMeasuredHeightAndState()两个方法获得。
b.向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果
onMeasure()算法的一些实现原则:
c.控件在进行测量时,控件需要将它的 Padding 尺寸计算在内,因为Padding是其尺寸的一部分。
d.ViewGroup在进行测量时,需要将子控件的 Margin 尺寸计算在内。因为子控件的Margin尺寸是父控件尺寸的一部分。
e.ViewGroup为子控件准备MeasureSpec时, SPEC_MODE应取决于子控件的LayoutParams.width/height的取值 。取值为MATCH_PARENT或一个确定的尺寸时应为EXACTLY,WRAP_CONTENT时应为AT_MOST。至于SPEC_SIZE,应理解为ViewGroup对子控件尺寸的限制,即ViewGroup按照其实现意图所允许子控件获得的最大尺寸。并且需要扣除子控件的Margin尺寸。
f.虽然说测量的目的在于确定尺寸,与位置无关。但是子控件的 位置 是ViewGroup进行测量时必须要首先 考虑 的。因为子控件的位置即决定了子控件可用的剩余尺寸,也决定了父控件的尺寸(当父控件的LayoutParams.width/height为WRAP_CONTENT时)。
g.在测量结果中 添加MEASURED_STATE_TOO_SMALL需要做到实事求是 。当一个方向上的空间不足以显示其内容时应考虑利用另一个方向上的空间,例如对文字进行换行处理,因为添加这个标记有可能导致父控件对其进行重新测量从而降低效率。
h.当子控件的测量结果中包含MEASURED_STATE_TOO_SMALL标记时,只要有可能, 父控件就应当调整给予子控件的MeasureSpec,并进行重新测量 。倘若没有调整的余地,父控件也应当将MEASURED_STATE_TOO_SMALL加入到自己的测量结果中,让它的父控件尝试进行调整。
i.ViewGroup在 测量子控件时必须调用子控件的measure()方法 ,而不能直接调用其onMeasure()方法。直接调用onMeasure()方法的最严重后果是子控件的PFLAG_LAYOUT_REQUIRED标识无法加入到mPrivateFlag中,从而导致子控件无法进行布局。
(5)是否改变窗口的尺寸
接下来回到performTraversals()方法。在ViewRootImpl.measureHierarchy()执行完毕之后,ViewRootImpl了解了控件树所需的空间。于是便可确定是否需要改变窗口窗口尺寸以便满足控件树的空间要求。前述的代码中多处设置windowSizeMayChange变量为true。windowSizeMayChange仅表示有可能需要改变窗口尺寸。而接下来的这段代码则用来确定窗口是否需要改变尺寸。
private void performTraversals() {
......// 测量控件树的代码
/* 标记mLayoutRequested为false。因此在此之后的代码中,倘若控件树中任何一个控件执行了
requestLayout(),都会重新进行一次“遍历” */
if (layoutRequested) {
mLayoutRequested = false;
}
// 确定窗口是否确实需要进行尺寸的改变
booleanwindowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight !=host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() !=mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() !=mHeight));
}
确定窗口尺寸是否确实需要改变的条件看起来比较复杂,这里进行一下总结,先介绍必要条件:
a: layoutRequested为true ,即ViewRootImpl.requestLayout()方法被调用过。View中也有requestLayout()方法。当控件内容发生变化从而需要调整其尺寸时,会调用其自身的requestLayout(),并且此方法会沿着控件树向根部回溯,最终调用到ViewRootImp.requestLayout(),从而引发一次performTraversals()调用。之所以这是一个必要条件,是因为performTraversals()还有可能因为控件需要重绘时被调用。当控件仅需要重绘而不需要重新布局时(例如背景色或前景色发生变化时),会通过invalidate()方法回溯到ViewRootImpl,此时不会通过performTraversals()触发performTraversals()调用,而是通过scheduleTraversals()进行触发。在这种情况下layoutRequested为false,即表示窗口尺寸不需发生变化。
b:windowSizeMayChange为true ,如前文所讨论的,这意味着WMS单方面改变了窗口尺寸而控件树的测量结果与这一尺寸有差异,或当前窗口为悬浮窗口,其控件树的测量结果将决定窗口的新尺寸。
在满足上述两个条件的情况下,以下两个条件满足其一:
c: 测量结果与ViewRootImpl中所保存的当前尺寸有差异。
d: 悬浮窗口的测量结果与窗口的最新尺寸有差异。
注意ViewRootImpl对是否需要调整窗口尺寸的判断是非常小心的。第4章介绍WMS的布局子系统时曾经介绍过,调整窗口尺寸所必须调用的performLayoutAndPlaceSurfacesLocked()函数会导致WMS对系统中的所有窗口新型重新布局,而且会引发至少一个动画帧渲染,其计算开销相当之大。因此ViewRootImpl仅在必要时才会惊动WMS。
至此,预测量阶段完成了。
参考:
《WindowManger与window进阶篇_1(ViewRootImpl深入理解,View测量)》
《 对Window/WindowManager和WindowManagerSystem的理解》
《 深入理解ViewRoot》