view系列疑惑之关于view的onMeasure执行两次的问题深层探究解析

前段时间做性能优化时偶然发现无论多简单的view都会执行两次onMeasure,很是诧异,网上其实也有分析过原因,链接地址:https://www.jianshu.com/p/733c7e9fb284
但我个人理解下还是有点出入的,所以来记录下。
由于传统的decorview到自己写的布局的view嵌套了很多层,所以debug非常的麻烦, 而在你真的了解popwindow和dialog么(二)https://www.jianshu.com/p/ba45eaa91b88文章里说过,其实新建popwindow的话,布局嵌套就外层的frameLayout+自己的布局了,所以创建一个popwindow,里面的布局如下


在弹出了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进行调用

image.png

值如下


image.png

间接调用了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方法
果然找到了另一处调用的地方


image.png

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 mViewAncestor;
        private final IWindowSession mWindowSession;

        W(ViewRootImpl viewAncestor) {
            mViewAncestor = new WeakReference(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方法

总结:

1.scheduleTraversals在window被add的时候其实是执行了3次的,每次的调用地方不同。
2.第一次执行scheduleTraversals会调用measure两次,而且都在layout之前执行的,这也是onMeaure会执两次的根本原因

你可能感兴趣的:(view系列疑惑之关于view的onMeasure执行两次的问题深层探究解析)