谈谈Activity的setContentView是怎么加载XML视图的
谈谈Activity的View怎么与View绘制工具ViewRootImpl关联的
在前面两篇文章中分析了View是如何跟绘制工具ViewRootImpl关联的,setContentView是如何把我们编写的xml视图添加到Activity中的,那今天就要分析下绘制工具ViewRootImpl是如何绘制我们Activity当中的View的。
首先要了解View是什么:
View代表了用户界面组件的基本构建块。一个View在屏幕上占据一个矩形区域,并负责绘图和事件处理。View是窗口小部件的基类,它是用于创建交互式的用户界面组件(按钮,文本等)
从前面两篇文章可知
- Activity的attach方法会构造一个PhoneWindow实例
- 我们在onCreate里通过setContentView将我们的xml添加到了DecorView
- ActivityThread在后续过程中会将DecorView添加到Activity的窗口中,也就是添加到PhoneWindow
- WindowManagerGlobal通过ViewRootImpl的setView方法将DecorView传递到ViewRootImpl进行绘制
我们来看下setView方法
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
……
requestLayout();;
}
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
可以看到这里post了一个mTraversalRunnable,我们看看这个runnable做了啥事
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
终于找到View绘制的最终入口了,也就是这个方法performTraversals()
performTraversals()
首先我们看这个方法名,perform是执行的意思,而Traversals是遍历循环的意思;所以这个方法看方法名就知道他是在遍历Activity的根布局DecorView里(或者其它窗口比如Dialog)的每一个View。
如果把Android中的View框架比作一个人,那我觉得这个方法就是人的心脏,非常重要。WMS的窗口属性变化,来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用,并在其中完成处理。View类及其子类中的onMeasure()、onLayout()以及onDraw()等回调也都是在performTraversals()的执行过程中直接或间接地引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊地工作着,一旦此方法无法正常执行,整个控件树都将处于僵死状态。
performTraversals方法可以说是我目前见到的Android源码中的几个最庞大的方法之一了,将近800行,如果你是第一次看这个肯定会迷糊的,没事,像我这样有时间看它个十几二十遍,你就会对它有个差不多的认识了。鉴于此方法太长,只能分段来看:
第一段:
//这个mView是通过setView方法传进来的,也就是Activity的根布局DecorView,使用final修饰,以防在遍历过程中被修改
final View host = mView;
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals");
host.debug();
}
//mAdded在setView方法修改为true,表明Decorview已经添加到了PhoneWindow
if (host == null || !mAdded)
return;
//是否正在遍历
mIsInTraversal = true;
//是否需要马上绘制
mWillDrawSoon = true;
//视图大小改变
boolean windowSizeMayChange = false;
//新视图
boolean newSurface = false;
//视图改变
boolean surfaceChanged = false;
WindowManager.LayoutParams lp = mWindowAttributes;
//Activity窗口的宽度和高度
int desiredWindowWidth;
int desiredWindowHeight;
//DecorView是否可见
final int viewVisibility = getHostVisibility();
final boolean viewVisibilityChanged = !mFirst
&& (mViewVisibility != viewVisibility || mNewSurfaceNeeded);
WindowManager.LayoutParams params = null;
if (mWindowAttributesChanged) {
mWindowAttributesChanged = false;
surfaceChanged = true;
params = lp;
}
CompatibilityInfo compatibilityInfo =
mDisplay.getDisplayAdjustments().getCompatibilityInfo();
if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
params = lp;
mFullRedrawNeeded = true;
mLayoutRequested = true;
if (mLastInCompatMode) {
params.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
mLastInCompatMode = false;
} else {
params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
mLastInCompatMode = true;
}
}
mWindowAttributesChangesFlag = 0;
//用来保存窗口宽度和高度,来自于全局变量mWinFrame,这个mWinFrame保存了窗口最新尺寸
Rect frame = mWinFrame;
//构造方法里mFirst赋值为true,意思是第一次执行遍历吗
if (mFirst) {
//是否需要重绘
mFullRedrawNeeded = true;
//是否需要重新确定Layout
mLayoutRequested = true;
//判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
//宽度和高度为整个屏幕的值
Configuration config = mContext.getResources().getConfiguration();
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
/**
* 因为第一次遍历,View树第一次显示到窗口
* 然后对mAttachinfo进行一些赋值
* AttachInfo是View类中的静态内部类AttachInfo类的对象
* 它主要储存一组当View attach到它的父Window的时候视图信息
*/
mAttachInfo.mUse32BitDrawingCache = true;//使用32位绘图缓存
mAttachInfo.mHasWindowFocus = false;//视图窗口当前不具有焦点。
mAttachInfo.mWindowVisibility = viewVisibility;//当前窗口不可见
mAttachInfo.mRecomputeGlobalAttributes = false;//ViewAncestor应在下次执行遍历时触发全局布局更改
mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
// 如果之前未设置布局方向,则设置布局方向(View.LAYOUT_DIRECTION_INHERIT 是默认值)
if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
host.setLayoutDirection(mLastConfiguration.getLayoutDirection());
}
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
//Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
} else {
//如果不是第一次进来这个方法,它的当前宽度和高度就从之前的mWinFrame获取
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
/**
* mWidth和mHeight是由WindowManagerService服务计算出的窗口大小,
* 如果这次测量的窗口大小与这两个值不同,说明WMS单方面改变了窗口的尺寸
*/
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
//需要进行完整的重绘以适应新的窗口尺寸
mFullRedrawNeeded = true;
//需要对控件树进行重新布局
mLayoutRequested = true;
//window窗口大小改变
windowSizeMayChange = true;
}
}
//如果窗口可见性变了,进行判断
if (viewVisibilityChanged) {
mAttachInfo.mWindowVisibility = viewVisibility;
host.dispatchWindowVisibilityChanged(viewVisibility);
host.dispatchVisibilityAggregated(viewVisibility == View.VISIBLE);
if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
//不可见就结束调整大小事件,释放相关硬件资源
endDragResizing();
destroyHardwareResources();
}
//如果窗口不可见,就修改标志位
if (viewVisibility == View.GONE) {
// After making a window gone, we will count it as being
// shown for the first time the next time it gets focus.
mHasHadWindowFocus = false;
}
}
// 如果窗口不可见了,去掉可访问性焦点
if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
host.clearAccessibilityFocus();
}
/**
* 执行HandlerActionQueue中HandlerAction数组保存的Runnable
* 我们平时会通过View.post()或View.postDelayed()方法将一个Runnable对象发送到主线程执行
* 其实就是通过这个mHandler去执行
* public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
*/
getRunQueue().executeActions(mAttachInfo.mHandler);
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
//进行预测量
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
if (mFirst) {
// 视图窗口当前是否处于触摸模式。
mAttachInfo.mInTouchMode = !mAddedTouchMode;
//确保这个Window的触摸模式已经被设置
ensureTouchModeLocally(mAddedTouchMode);
} else {
/**
* 判断一下几个insects的值和上一次相比有没有什么变化,不同的话就改变insetsChanged
* mOverscanInsets 记录屏幕中的 overscan 区域 见贴图中相应描述区域
* mContentInsets 记录了屏幕中的控件在布局时必须预留的空间 见贴图中相应描述区域
* mStableInsets 记录了Stable区域,比mContentInsets区域大 见贴图中相应描述区域
* mVisibleInsets 记录了被遮挡的区域,如正在进行输入的TextView等不被遮挡,这样VisibleInsets的变化并不会导致重新布局,
* 所以这里仅仅是将VisibleInsets保存到mAttachInfo中,以便绘制时使用
*/
if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
insetsChanged = true;
}
if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
insetsChanged = true;
}
if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
insetsChanged = true;
}
if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: "
+ mAttachInfo.mVisibleInsets);
}
if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
insetsChanged = true;
}
if (mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar) {
insetsChanged = true;
}
/**
* 如果当前窗口的根布局的width或height被指定为WRAP_CONTENT时,
* 比如Dialog,那我们还是给它尽量大的长宽,这里是将屏幕长宽赋值给它
*/
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
//判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
Configuration config = res.getConfiguration();
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
}
}
// 进行预测量窗口大小,以达到更好的显示大小
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
到这里主要计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。
注意:
通过上面的代码可以看到desiredWindowWidth和desiredWindowHeight是经过一系列代码考核得到的值,讲道理整个View树是可以按照这个值去测量的,但是我们不仅只是测量和布局,还要尽可能舒适的一个UI去展示给用户
比如在大屏幕上,Dialog的width修饰为WRAP_CONTENT,按照上面的代码,其实desiredWindowWidth还是给了尽量大的值,就是屏幕宽度;但是Dialog可能就是为了显示几个字,那结果就是整个dialog的布局就被拉伸铺满屏幕;显然这种UI是不美丽的,那就需要通过measureHierarchy方法去优化,尝试下更小的宽度是否合适。就这样来到了measureHierarchy方法。
measureHierarchy
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
//用于描述宽度的MeasureSpec
int childWidthMeasureSpec;
//用于描述高度的MeasureSpec
int childHeightMeasureSpec;
//表示测量结果是否可能导致窗口的尺寸发生变化
boolean windowSizeMayChange = false;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag,
"Measuring " + host + " in display " + desiredWindowWidth
+ "x" + desiredWindowHeight + "...");
//表示测量是否能满足控件树充分显示内容的要求
boolean goodMeasure = false;
//其实测量协商仅仅发生在width被指定为WRAP_CONTENT情况
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 进入到这里就让窗口的大小不以屏幕大小去布局,而是给一个固定的值看看是否合适
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
//取出一个固定值来用,来自于config_prefDialogWidth
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize
+ ", desiredWindowWidth=" + desiredWindowWidth);
//假设上面config_prefDialogWidth值是320dp
if (baseSize != 0 && desiredWindowWidth > baseSize) {
//使用getRootMeasureSpec()函数组合SPEC_MODE与SPEC_SIZE为一个MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第一次测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight()
+ ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
+ " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
/**
* 控件树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。
* 如果控件树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位
*/
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
/**
* 走到这里说明上面的尺寸不适合View树
* 需要对宽度再进行放松,使用上面的预期值与最大值的平均值作为新的宽度
*/
baseSize = (baseSize+desiredWindowWidth)/2;
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="
+ baseSize);
//重新获取MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//第二次测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
//再次判断View树对新的结果是否满意
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
if (DEBUG_DIALOG) Log.v(mTag, "Good!");
goodMeasure = true;
}
}
}
}
/**
* 如果上面两次尝试测量结果,View树都不满意
* 那老哥也没办法了,做不了优化了,只能以屏幕宽高去给View树测量了
* 这是第三次也是最后一次测量了
*/
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果测量得出的结果与当前窗口值不一样,就需要调整窗口大小了
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after measure");
host.debug();
}
return windowSizeMayChange;
}
这里不属于三大流程,只是一个确定显示窗口的大小。
可以看到这个方法进行了两次优化处理,如果两次优化处理还是不满意,就使用给定的desiredWindowWidth/desiredWindowHeight进行测量;这里也表明了一点,performMeasure方法可能会被调用多次,那onMeasure()方法同样会被回调多次,这样我们在自定义View的时候,就不要在这个回调方法里做过多的new内存操作。
注意到方法里会调用到getRootMeasureSpec这么一个方法,说到这个方法就得先谈下MeasureSpec这个类,它是View类的一个静态内部类:
- MeasureSpec封装了从父级传递给子级的布局要求
- 每个MeasureSpec代表宽度或高度的要求
MeasureSpec由大小和模式组成。 有三种可能
1.UNSPECIFIED :父View没有对子View做任何限制,子View可以是自己想要的尺寸;像我们平时在xml中写View的时候没有设置宽高,这种情况比较少见。它的值是0 << 30 = 0
2.EXACTLY:父View决定了子View确切大小,子View将被限定在给定的边界里;像xml中子view如果是填充父窗体(match_parent)或者确定大小,说明父View已经明确知道子控件想要多大的尺寸了。它的值是1 << 30 = 1073741824
3.AT_MOST:子View可以是它所期望的尺寸,但是不能大于specSize;在布局设置wrap_content,父控件并不知道子控件到底需要多大尺寸(具体值), 需要子控件在measure测量之后再让父控件给他一个尽可能大的尺寸以便让内容全部显示;如果在onMeasure没有指定控件大小,默认会填充父窗体,因为在view的measure源码中, AT_MOST(相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize, 而这个specSize正是父控件剩余的宽高,所以默认measure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。它的值是2 << 30 = -2147483648
所以这个类是SPEC_SIZE和SPEC_MODE的组合,结构如下
SPEC_MODE(32,31) - SPEC_SIZE(30,…,1)
也就是高两位表示模式(在父View中是如何定义的),低30位表示大小(父View的建议尺寸)
再来看下performMeasure方法,它有两个参数,一个是包含View的宽度度量规范,一个是包含View的高度度量规范,然后根据这两个宽高的度量规范去测量View所需的大小;那这两个信息怎么来呢,就是通过getRootMeasureSpec获取,这个方法会调用MeasureSpec类的makeMeasureSpec方法,根据给定的大小和模式返回一个度量规范
getRootMeasureSpec
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
既然宽高的度量规范出来了,那就开始测量了,调用performMeasure方法
performMeasure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这里会直接调用View的measure方法
measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//判断当前View是否是以可见边界布局的ViewGroup
boolean optical = isLayoutModeOptical(this);
//对度量规范进行校正
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// 先清除测量尺寸标记
// PFLAG_MEASURED_DIMENSION_SET标记用于检查控件在onMeasure()方法中是否通过
//调用setMeasuredDimension()将测量结果存储下来
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
//解析所有与RTL相关的属性,比如背景,字体,padding等属性设置
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//对自己进行测量, 每个View子类都需要重写这个方法以便正确地对自身进行测量
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
/**
* 检查View子类的onMeasure()是否调用了setMeasuredDimension()
* setMeasuredDimension()会将PFLAG_MEASURED_DIMENSION_SET标记重新加入mPrivateFlags中。
* 之所以做这样的检查,是由于onMeasure()的实现可能由开发者完成,
* 而在Android看来,开发者是不可信的
*/
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
//记录父控件给予的MeasureSpec,用以检查之后的测量操作是否有必要进行
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
可以看到其实这个方法并没有做任何的测量工作,真正的测量过程交给了onMeasure方法去做,它的作用在于引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。另外,在控件系统看来,一旦控件执行了测量操作,那么随后必须进行布局操作,因此在完成测量之后,将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags,以便View.layout()方法可以顺利进行
onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到这个方法实现很简单,只调用了setMeasuredDimension()方法保存测量结果,具体的实现由子类去重写,提供更加合理、高效的实现
setMeasuredDimension
必须通过onMeasure调用此方法来存储测量的宽度和测量的高度。
如果不这样做,将在测量时触发异常
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//保存测量的宽度
mMeasuredWidth = measuredWidth;
//保存测量的高度
mMeasuredHeight = measuredHeight;
//向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
其实这里的逻辑就是我们自定义View的时候,重写onMeasure方法,我们自己实现测量过程,测量结束后然后调用setMeasuredDimension方法将测量结果传入进行保存。
两个测量结果可以通过getMeasuredWidthAndState()与getMeasuredHeightAndState()两个方法获得。测量结果不仅仅是一个尺寸,而是一个测量状态与尺寸的复合整型值。其低30位表示了测量结果的尺寸,而高两位则表示了控件对测量结果是否满意,即父控件给予的MeasureSpec是否可以使得子控件完整地显示其内容。当控件对测量结果满意时,直接将尺寸传递给setMeasuredDimension()即可,注意要保证高两位为0。倘若对测量结果不满意,则使用View.MEASURED_STATE_TOO_SMALL | measuredSize 作为参数传递给setMeasuredDimension()以告知父控件对MeasureSpec进行可能的调整。回看measureHierarchy方法能看到调用getMeasuredWidthAndState,来判断是否对测量结果是否满意。
执行完measureHierarchy方法后,ViewRootImpl就知道了View树需要的大小,并同时修改windowSizeMayChange的值,它的表示有可能需要改变窗口大小以适应View树的大小要求,接下来的一段代码就正式确定是否需要改变窗口大小
/**
* 用于确定是否需要更改窗口大小用来适用View树的空间要求
* layoutRequested 为true,说明view正在调用自身的requestLayout,自身内容变化要求重新布局,
* 随后此方法会沿着控件树向根部回溯,最终调用到ViewRootImp.requestLayout()
* windowSizeMayChange 为true ,它有多处赋值,说明View树所需大小与窗口大小不一致
* (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() 判断上面测量后View树的大小与窗口大小值是否相等
* 最后的条件是如果窗口width或height被设置成WRAP_CONTENT,计算出来的窗口大小desiredWindowWidth/desiredWindowHeight 与上一次测量保存的frame.width()/frame.height()
* 同时与WindowManagerService服务计算的宽度mWidth和高度mHeight也不一致,那也说明窗口大小变化了
*/
boolean windowShouldResize = 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));
windowShouldResize |= mDragResizing && mResizeMode == RESIZE_MODE_FREEFORM;
// If the activity was just relaunched, it might have unfrozen the task bounds (while
// relaunching), so we need to force a call into window manager to pick up the latest
// bounds.
//如果Activity重新启动,需要强制通过wms获取最新的值
windowShouldResize |= mActivityRelaunched;
第二阶段
/**
* 开始进入三大阶段的第一个阶段了 6个条件只要满足一个就进入
* mFirst true 说明是第一次执行测量布局绘制操作
* windowShouldResize true 即Activity窗口大小需要改变
* insetsChanged true 说明此次窗口overscan等一些边衬区域发生了改变,与上次不一样
* viewVisibilityChanged 说明View的可见性发生了变化
* params 即窗口属性发生了变化,指向了mWindowAttributes
* mForceNextWindowRelayout true 即ViewRootHandler接收到消息MSG_RESIZED_REPORT,即size改变了
*/
final boolean isViewVisible = viewVisibility == View.VISIBLE;
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
mForceNextWindowRelayout = false;
......
......
try {
//请求WMS计算Activity窗口大小及边衬区域大小
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
......
......
/**
* mPendingOverscanInsets等rect在relayoutWindow方法里保存了最新窗口大小值
* 再与上一次测量的保存在mAttachInfo中的值进行比较
*/
final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals(
mAttachInfo.mOverscanInsets);
boolean contentInsetsChanged = !mPendingContentInsets.equals(
mAttachInfo.mContentInsets);
final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals(
mAttachInfo.mVisibleInsets);
final boolean stableInsetsChanged = !mPendingStableInsets.equals(
mAttachInfo.mStableInsets);
final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
final boolean surfaceSizeChanged = (relayoutResult
& WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0;
final boolean alwaysConsumeNavBarChanged =
mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar;
//如果改变了,就重新将最新值保存在mAttachInfo中
if (contentInsetsChanged) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Content insets changing to: "
+ mAttachInfo.mContentInsets);
}
if (overscanInsetsChanged) {
mAttachInfo.mOverscanInsets.set(mPendingOverscanInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Overscan insets changing to: "
+ mAttachInfo.mOverscanInsets);
// Need to relayout with content insets.
contentInsetsChanged = true;
}
if (stableInsetsChanged) {
mAttachInfo.mStableInsets.set(mPendingStableInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Decor insets changing to: "
+ mAttachInfo.mStableInsets);
// Need to relayout with content insets.
contentInsetsChanged = true;
}
if (alwaysConsumeNavBarChanged) {
mAttachInfo.mAlwaysConsumeNavBar = mPendingAlwaysConsumeNavBar;
contentInsetsChanged = true;
}
if (contentInsetsChanged || mLastSystemUiVisibility !=
mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested
|| mLastOverscanRequested != mAttachInfo.mOverscanRequested
|| outsetsChanged) {
mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
mLastOverscanRequested = mAttachInfo.mOverscanRequested;
mAttachInfo.mOutsets.set(mPendingOutsets);
mApplyInsetsRequested = false;
dispatchApplyInsets(host);
}
if (visibleInsetsChanged) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: "
+ mAttachInfo.mVisibleInsets);
}
......
......
// !!FIXME!! This next section handles the case where we did not get the
// window size we asked for. We should avoid this by getting a maximum size from
// the window session beforehand.
//从window session获取最大size作为当前窗口大小
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
......
......
/**
* mStopped true 说明当前窗口的所有者 比如activity处于暂停状态
* mReportNextDraw true 即ViewRootHandler接收到消息MSG_RESIZED_REPORT ,需要绘制
*/
if (!mStopped || mReportNextDraw) {
//确定当前窗口处于触摸模式,也就是获取焦点
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || framesChanged ||
updatedConfiguration) {
//获取宽高的度量规范
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed! mWidth="
+ mWidth + " measuredWidth=" + host.getMeasuredWidth()
+ " mHeight=" + mHeight
+ " measuredHeight=" + host.getMeasuredHeight()
+ " framesChanged=" + framesChanged);
// 执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
//获取测量后的View的真是宽度和高度
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
//是否需要重新测量标志
boolean measureAgain = false;
//如果测量出来的水平宽度需要拉伸 需要重新测量
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
//再次测量
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(mTag,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
} else {
// 判断窗口有没有移动,如果移动就执行移动动画
maybeHandleWindowMove(frame);
}
第二阶段也就是我们View绘制三大流程第一步了,具体测量代码分析在第一阶段已经分析了,就不在赘述
第三阶段
//通过这几个值来确定是不是要重新布局
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
// By this point all views have been sized and positioned
// We can compute the transparent area
//计算透明区域
if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
// start out transparent
// TODO: AVOID THAT CALL BY CACHING THE RESULT?
host.getLocationInWindow(mTmpLocation);
mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + host.mRight - host.mLeft,
mTmpLocation[1] + host.mBottom - host.mTop);
host.gatherTransparentRegion(mTransparentRegion);
if (mTranslator != null) {
mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
}
if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
mPreviousTransparentRegion.set(mTransparentRegion);
mFullRedrawNeeded = true;
// reconfigure window manager
try {
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
} catch (RemoteException e) {
}
}
}
}
performLayout方法会调用View的layout方法,在layout方法中回调onLayout方法,开发者可以重写这个方法,这个具体分析与measure类似
第四阶段
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
//没有取消绘制 也没有重新创建Surface
if (!cancelDraw && !newSurface) {
//执行动画效果
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
//开始绘制
performDraw();
}
在View的Draw方法里绘制一个View分一下几步
- drawBackground 绘制背景
- 保存画布图层
- onDraw(canvas) 回调 绘制内容 由子类去实现
- dispatchDraw(canvas) 有子view 就绘制子view
- 绘制淡入淡出效果并恢复图层
- onDrawForeground(canvas) 画装饰 比如前景色 滚动条
这里就谈下我们的Activity,Activity启动后,它的DecorView会被ViewRootImpl类的performTraversals方法去操作;
首先就是performMeasure的过程,这个方法会调用到View类的measure方法,这里并没有具体的测量实现,而是调用到了onMeasure,需要子类去实现;要知道DecorView其实就是个FrameLayout,是一个ViewGroup,更是一个View,自己重写onMeasure方法,去循环调用子View的measure过程;子类也会重写onMeasure方法,自己实现测量自己的大小
这样所有的View测量结束,第二步就是performLayout了,默认是调用到了View类layout方法;如果是ViewGroup,就调用ViewGroup的layout方法,但是这个方法没有具体实现,还是调用到了View类的layout方法;View类的layout方法回调onLayout方法,要怎么确定位置由子类重写onlayout方法实现;这里DecorView去重写,自己实现,循环调用每个子View的layout方法,将上下左右坐标值传递给子View,确定子View的位置
接下来就是最后的绘制过程了,也就是第三步performDraw,继续走到ViewTreeObserver的dispatchOnDraw方法,通知所有注册了OnDrawListener接口的View去调用自己的onDraw()方法,绘制自己;DecorView因为是个ViewGroup,重写了dispatchDraw方法,循环调用每个子View的draw方法;最后就是每个子View重写onDraw方法绘制自身