Window,WindowManager详解
先上一张uml关系图
在这篇文章中《 WindowManger与window之基础篇 》详细讲解了添加Window的过程:
第一步:创建Window 这个是在Activity#attach方法中创建的;
第二步:创建DecorView(顶层View),在PhoneView#setContentVIew的方法中 创建 的;
第三步:window添加DecorView,在 ActivityThead#handleResumeActivity方法中实现的;
第四步:添加 window到手机屏幕上,大致过程如下:
WindowManager——>WindowManagerGobal——>ViewRootImpl——>Session——>WindowManagerService。
ViewManager接口的另一个实现者是ViewGroup,它是容器类控件的基类,用于将一组控件容纳到自身的区域中,这一组控件被称为子控件。ViewGroup可以根据子控件的布局参数(LayoutParams)在其自身的区域中对子控件进行布局。
设想WindowManager是一个ViewGroup,其区域为整个屏幕,而其中的各个窗口就是一个一个的View。WindowManager通过WMS(即WindowManagerService)的帮助将这些View按照其布局参数(LayoutParams)将其显示到屏幕的特定位置。二者的核心工作是一样的,因此WindowManager与ViewGroup都继承自ViewManager。那么就可以 WindowManager看作是一个ViewGroup。
1.WindowManagerService是Android中一个重要的服务(Service )。WindowManagerService是全局的,是唯一的。它将用户的操作,翻译成为指令,发送给呈现在界面上的各个Window。Activity会将顶级的控件注册到 WindowManager 中.
WindowManager继承自ViewManager,ViewManager是一个接口,代码如下
public interface ViewManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
2.windowManager 里一个重要的内部类就是 LayoutParams , LayoutParams 继承 ViewGroup.LayoutParams
实现了 Parcelable接口
layoutParams 里边定义了window的层级关系.代码如下
public static final int TYPE_BASE_APPLICATION = 1;
public static final int TYPE_APPLICATION = 2;
public static final int LAST_APPLICATION_WINDOW = 99;
public static final int LAST_SUB_WINDOW = 1999;
public static final int FIRST_SYSTEM_WINDOW = 2000;
其中.应用Window的层级范围是1-99,子Window的层级是1000-1999,系统Window的层级是2000-2999.想让window位于所有window的最顶层,采用较大的层级就可以.使用系统层级的window需要在权限中声名对应的window权限如
<user-permission android:name=”android.permission.SYSTEM_ALERT_WINDOW”/>
3.window 是一个抽象概念.每个window都对应着一个View和ViewRootImpl,Window和View通过ViewRootImpl来建立联系.下边分别讲解
window的添加过程
window的add,remove,update过程都有WindowManager来实现,WindowManager是一个接口,具体实现类是WindowManagerImpl类.WindowManagerImpl 代码 public WindowManagerImpl(Display display) { this(display, null); } private WindowManagerImpl(Display display, Window parentWindow) { mDisplay = display; mParentWindow = parentWindow; } private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); @Override public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } @Override public void updateViewLayout(View view, ViewGroup.LayoutParams params) { mGlobal.updateViewLayout(view, params); } @Override public void removeView(View view) { mGlobal.removeView(view, false); } @Override public void removeViewImmediate(View view) { mGlobal.removeView(view, true); }
1.WindowManagerImpl除了保存了窗口所属的屏幕以及父窗口以外,没有任何实质性的工作。窗口的管理都交由WindowManagerGlobal的实例完成。 详细流程可参考这篇文章《android获取Service过程》。WindowManagerImpl 通过 WindowManagerGlobal 来对View进行操作. WindowManagerGlobal通过单例模式提供他自己的对象.
1 | //WindowManagerGlobal 代码 |
2.接下来讲解 WindowManagerGlobal,他里边有几个重要参数先列出来
// WindowManagerGlobal 代码
private static IWindowManager sWindowManagerService;
private static IWindowSession sWindowSession; //前两个先不看
private final Object mLock = new Object(); //线程的锁
//所有Window对应的view的集合
private final ArrayList<View> mViews = new ArrayList<View>();
//所有window对应的ViewRootImpl 的集合
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//所有Window对应的布局参数集合
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
//所有调用removeView方法但删除操作还没完成的View集合
private final ArraySet<View> mDyingViews = new ArraySet<View>();
3.看WindowManagerGolbal的addView方法
//WindowManagerGolbal 代码
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ------- 1.先检查参数是否合法,
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
if (parentWindow != null) { //调整parentWindow的param参数
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent and we're running on L or above (or in the
// system context), assume we want hardware acceleration.
final Context context = view.getContext();
if (context != null //开启硬件加速
&& context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();//ViewRootImpl 加载系统属性
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);//监听系统属性变化
}
int index = findViewLocked(view, false);//看这个view是否存在于mViews里
if (index >= 0) {
if (mDyingViews.contains(view)) { //如果已经添加到mViews里,看看是否是在待删的mDyingViews里,是的话直接删除
,不是的话,就抛出异常.
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
//如果是面板view,就找到他第一次attach的window,以备以后查询.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
//------- 2.生成一个新的viewRootImpl,并且把 view. viewrootImpl, param 都保存起来.
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
//------- 3.调用ViewRoomImpl来更新界面并完成Window的添加过程
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
-------4.在 ViewRootImpl的setview方法里主要是一些对LayoutParams的处理,也看不太懂.不过其中重要是调用了requestLayout();
进行重新绘制.然后调用 mWindowSession完成view的添加过程.
//ViewRoomImpl内部成员
final IWindowSession mWindowSession;
//VewRoomImpl 的 setView 方法
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
}
其实,mWindowSession的类型是IWindowSession,他是一个Binder对象,真正的实现类是Session.也就是Window的一次添加过程是一次IPC调用
1 | //Session代码 |
这样,Window的一次添加过程就由WindowManagerService去处理了,而WindowManagerService内部会为每一个应用并保留一个单独的session
Window的删除过程
从 WindowManager的实现类WindowManagerImpl 看起//windowManagerImpl代码 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); @Override public void removeView(View view) { mGlobal.removeView(view, false); }
接着看 WindowManagerGlobal
1 | //WindowManagerGlobal 代码 为了方便.把之前列出的WindowManagerGlobal 几个成员写下 |
// 接着看 removeViewLocked
//WindowManagerGlobal 方法 ,immediate 标识是否立即删除.
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (view != null) {
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken()); //这是一个调用InputManagerService的ipc的过程
}
}
boolean deferred = root.die(immediate); //通过ViewRootImpl 完成view的删除
if (view != null) {
view.assignParent(null);
if (deferred) { //如果是延时的remove view,则把view 添加到mDyingViews里
mDyingViews.add(view);
}
}
}
接下来追中到ViewRootImpl里的die方法
//ViewRootImpl代码 immediate
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie(); //直接执行
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(TAG, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE); //其实就是在handler在处理消息时会调用 dodie方法
return true;
}
//继续看dodie方法 ViewRootImpl代码
void doDie() {
checkThread(); //确保mThread=Thread.currentThread() //其实mThread初始化就是他
if (LOCAL_LOGV) Log.v(TAG, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) { //标记变量.标识view是否被删除
return;
}
mRemoved = true;
if (mAdded) {
dispatchDetachedFromWindow();//这是主要的删除方法
}
if (mAdded && !mFirst) {
destroyHardwareRenderer(); //销毁硬件渲染器
if (mView != null) {
int viewVisibility = mView.getVisibility();
boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
if (mWindowAttributesChanged || viewVisibilityChanged) {
// If layout params have been changed, first give them
// to the window manager to make sure it has the correct
// animation info.
try {
if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
mWindowSession.finishDrawing(mWindow); //用来更新window的layoutparams
}
} catch (RemoteException e) {
}
}
mSurface.release();
}
}
mAdded = false;
}
//这句代码主要是通过本ViewRoomImpl在WindowManagerGlobal mRoots中的index.
//分别在mRoots,mParams,mDyingViews 把这个index对应的不同数据都删除
WindowManagerGlobal.getInstance().doRemoveView(this);
}
最后 ViewRootImpl中注意做了以下的事
1.垃圾回收相关,清楚数据,和消息,移除回调
2.通过Session的remove方法删除Window,其实是通过IPC调用WindowManagerService的removeWindow方 法.
3.调用View的diapatchDetachedfromWindow 方法,内部会回调View的onDetachedFromWindow及onDetachedFromWindowInternal(),
到此.remove过程完毕
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》