前段时间做性能优化时偶然发现无论多简单的view都会执行两次onMeasure,很是诧异,网上其实也有分析过原因,链接地址:https://www.jianshu.com/p/733c7e9fb284
但我个人理解下还是有点出入的,所以来记录下。
由于传统的decorview到自己写的布局的view嵌套了很多层,所以debug非常的麻烦, 而在你真的了解popwindow和dialog么(二)https://www.jianshu.com/p/ba45eaa91b88文章里说过,其实新建popwindow的话,布局嵌套就外层的frameLayout+自己的布局了,所以创建一个popwindow,里面的布局如下
<com.fish.viewpractise.TestTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="弹出的window"
android:textSize="20dp" />
在弹出了popwindow时确实打印了2次的onMeasure,本人时api28的,可能在其他版本上略有不同
11-24 19:51:00.370 22232-22232/com.fish.myscrollviewpractise I/TestTextView: ============onMeasure
11-24 19:51:00.385 22232-22232/com.fish.myscrollviewpractise I/TestTextView: ============onMeasure
============onLayout=====true=====0=====0=====500=====200
11-24 19:51:00.390 22232-22232/com.fish.myscrollviewpractise I/TestTextView: ============onDraw
下面来分析下原因:
大家都知道引起第一次测量的原因是addView时走到了viewRootImp的setView里,然后调用了requestLayout来进行绘制的流程.
先看onMeasure是如何被调用的,在viewRootImp里会调用doTraversal方法,然后调用measureHierarchy进行调用
值如下
间接调用了view的meaure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
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) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
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;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
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;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
可以看到measure里面其实是做了2级的判断的,来区分是否要调用onMeasure这个方法
1.首先是forceLayout这个标志位,这个在view的requstLayout时其子view和它的父view都会带上这个标志
2.宽高发生变化时也会去调用,由log所知,两次测量的结果是一样的
而forceLayout这个标志是在view进行过layout后会自动清除这个标志
那就是说只有一种可能了
谷歌的代码里在doTraversal时第一次的时候连续调用了2次的measure方法。
不然如果不在一个handler分发中调用的话,是不可能调用2次onMeasure的
而且第二次的onMeasure一定是在onLayout之前
所以将debug调至了performMeasure方法
果然找到了另一处调用的地方
debug由于代码的偏差导致了行数不对,将就看吧
这里的两个判断mStopped和mReportNextDraw其实都能进来,关键是下面的判断,很明显decorview的宽高是等于viewRootImp的宽高的,而只有一个contentInsetChange这个参数决定是否能够测量了
这个参数在上文是被赋值过的
final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals(
mAttachInfo.mOverscanInsets);
contentInsetsChanged = !mPendingContentInsets.equals(
mAttachInfo.mContentInsets);
final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals(
mAttachInfo.mVisibleInsets);
final boolean stableInsetsChanged = !mPendingStableInsets.equals(
mAttachInfo.mStableInsets);
final boolean cutoutChanged = !mPendingDisplayCutout.equals(
mAttachInfo.mDisplayCutout);
final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
final boolean surfaceSizeChanged = (relayoutResult
& WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0;
surfaceChanged |= surfaceSizeChanged;
final boolean alwaysConsumeNavBarChanged =
mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar;
if (contentInsetsChanged) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Content insets changing to: "
+ mAttachInfo.mContentInsets);
contentInsetsChanged = true;
}
很明显是mPendingOverscanInsets的值和mAttachInfo的值不一样contentInsetsChanged才会返回true,默认mAttachInfo的OverscanInsets是(0,0,0,0),那mPendingOverscanInsets这个值其实是在和windowSession的aidl通讯中,返回回来的
if (mAttachInfo.mThreadedRenderer != null) {
// relayoutWindow may decide to destroy mSurface. As that decision
// happens in WindowManager service, we need to be defensive here
// and stop using the surface in case it gets destroyed.
if (mAttachInfo.mThreadedRenderer.pauseSurface(mSurface)) {
// Animations were running so we need to push a frame
// to resume them
mDirty.set(0, 0, mWidth, mHeight);
}
mChoreographer.mFrameInfo.addFlags(FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED);
}
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
其中的relayoutWindow是关键
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
float appScale = mAttachInfo.mApplicationScale;
boolean restore = false;
if (params != null && mTranslator != null) {
restore = true;
params.backup();
mTranslator.translateWindowLayout(params);
}
if (params != null) {
if (DBG) Log.d(mTag, "WindowLayout in layoutWindow:" + params);
if (mOrigWindowType != params.type) {
// For compatibility with old apps, don't crash here.
if (mTargetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
Slog.w(mTag, "Window type can not be changed after "
+ "the window is added; ignoring change of " + mView);
params.type = mOrigWindowType;
}
}
}
long frameNumber = -1;
if (mSurface.isValid()) {
frameNumber = mSurface.getNextFrameNumber();
}
int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurface);
mPendingAlwaysConsumeNavBar =
(relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_NAV_BAR) != 0;
if (restore) {
params.restore();
}
if (mTranslator != null) {
mTranslator.translateRectInScreenToAppWinFrame(mWinFrame);
mTranslator.translateRectInScreenToAppWindow(mPendingOverscanInsets);
mTranslator.translateRectInScreenToAppWindow(mPendingContentInsets);
mTranslator.translateRectInScreenToAppWindow(mPendingVisibleInsets);
mTranslator.translateRectInScreenToAppWindow(mPendingStableInsets);
}
return relayoutResult;
}
这里返回值其实就是状态栏和虚拟按键的高度了,所以在第一次的时候要进行2次的测量,后面当 mAttachInfo.mOverscanInsets有值的时候,就不会进去了。
那其实2次的onMeasure问题是解决了,那到底执行了多少次的doTraversal呢?
这里我加了一个view.post的时机,看一下view的post是在何时执行的
11-24 21:16:20.773 25111-25111/com.fish.myscrollviewpractise I/TestTextView: ============onMeasure
11-24 21:16:20.788 25111-25111/com.fish.myscrollviewpractise I/TestTextView: ============onMeasure
11-24 21:16:20.791 25111-25111/com.fish.myscrollviewpractise I/TestTextView: ============onLayout=====true=====0=====0=====500=====200
11-24 21:16:20.792 25111-25111/com.fish.myscrollviewpractise I/TestTextView: ============onPost
11-24 21:16:20.812 25111-25111/com.fish.myscrollviewpractise I/TestTextView: ============onDraw
这个log很多人就会奇怪了,post应该是在绘制完成以后的一个handler执行的,按理说应该是在onDraw的后面
这样的log就说明了onDraw和onLayout不在一个handler里执行的
而是在view.post的handler后面执行的,在源码里可以看到
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
mIsInTraversal = false;
其实第一次执行的是scheduleTraversals方法, 也就是说newSurface这个值是返回true的
if (!hadSurface) {
if (mSurface.isValid()) {
// If we are creating a new surface, then we need to
// completely redraw it. Also, when we get to the
// point of drawing it we will hold off and schedule
// a new traversal instead. This is so we can tell the
// window manager about all of the windows being displayed
// before actually drawing them, so it can display then
// all at once.
newSurface = true;
mFullRedrawNeeded = true;
mPreviousTransparentRegion.setEmpty();
// Only initialize up-front if transparent regions are not
// requested, otherwise defer to see if the entire window
// will be transparent
if (mAttachInfo.mThreadedRenderer != null) {
try {
hwInitialized = mAttachInfo.mThreadedRenderer.initialize(
mSurface);
if (hwInitialized && (host.mPrivateFlags
& View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0) {
// Don't pre-allocate if transparent regions
// are requested as they may not be needed
mSurface.allocateBuffers();
}
} catch (OutOfResourcesException e) {
handleOutOfResourcesException(e);
return;
}
}
}
} else if (!mSurface.isValid()) {
看代码的意思是第一次mSurface还是无效的,大家都知道canvas的本质就是mSurface,而mSurface其实就是将openGL或者skia绘制的内容放到了缓冲区去,在从surfacefling中从离屏的缓冲区中取出来,当surface是无效的,那canvas当然不能绘制了,我猜想mThreadedRenderer .initialize和mSurface.allocateBuffers方法应该也是在native发送native的消息的,所以第一次draw一定要用handler的消息队列的。
那为啥后面没有调用onMeasure和onLayout呢?
其实后面的一次handler进行时,他们都做了优化,所以不会再调用了
那也就是说scheduleTraversals被调用了2次么?其实不是,根据debug所知道情况其实是3次,最后一次是由
case MSG_RESIZED_REPORT:
if (mAdded) {
SomeArgs args = (SomeArgs) msg.obj;
final int displayId = args.argi3;
MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg4;
final boolean displayChanged = mDisplay.getDisplayId() != displayId;
if (!mLastReportedMergedConfiguration.equals(mergedConfiguration)) {
// If configuration changed - notify about that and, maybe,
// about move to display.
performConfigurationChange(mergedConfiguration, false /* force */,
displayChanged
? displayId : INVALID_DISPLAY /* same display */);
} else if (displayChanged) {
// Moved to display without config change - report last applied one.
onMovedToDisplay(displayId, mLastConfigurationFromResources);
}
final boolean framesChanged = !mWinFrame.equals(args.arg1)
|| !mPendingOverscanInsets.equals(args.arg5)
|| !mPendingContentInsets.equals(args.arg2)
|| !mPendingStableInsets.equals(args.arg6)
|| !mPendingDisplayCutout.get().equals(args.arg9)
|| !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);
mPendingDisplayCutout.set((DisplayCutout) args.arg9);
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) {
reportNextDraw();
}
if (mView != null && framesChanged) {
forceLayout(mView);
}
requestLayout();
}
这个发出的handler是在
static class W extends IWindow.Stub {
private final WeakReference<ViewRootImpl> mViewAncestor;
private final IWindowSession mWindowSession;
W(ViewRootImpl viewAncestor) {
mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
mWindowSession = viewAncestor.mWindowSession;
}
@Override
public void resized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout,
boolean alwaysConsumeNavBar, int displayId,
DisplayCutout.ParcelableWrapper displayCutout) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
viewAncestor.dispatchResized(frame, overscanInsets, contentInsets,
visibleInsets, stableInsets, outsets, reportDraw, mergedConfiguration,
backDropFrame, forceLayout, alwaysConsumeNavBar, displayId, displayCutout);
}
}
也就是windowManagerServer给window的回调,执行了dispatchResized才会执行的,当然这一次没有调用任何的onMeasure或者是onLayout或者onDraw方法
总结: