How does Android window determine the reason why / onmeasure() is executed multiple times

Time:2021-12-6

Change the article to reprint source:
Author: fishforest
Link:https://www.jianshu.com/p/6e45f42da304
Source: developeppaper
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

preface

The view measure process was systematically analyzed previously:
Measure process of Android custom view
We know that the parent layout generates the measurement mode and measurement size for the child layout according to its own and child layout requirements, which are encapsulated in the measurespec object, and finally passed to the child layout to determine its own size.
Naturally, since the child layout takes the measurement results from the parent layout, the parent layout takes the measurement results from its parent layout. Finally, who measures the vertex root view of the viewtree?
Follow this question and find out from the perspective of source code.

Series of articles:

Window / WindowManager must know
How does Android window determine the reason why / onmeasure() is executed multiple times

Through this article, you will learn:

1. Window dimension measurement
2. Root dimension measurement
3. Relationship among window, viewrootimpl and view

1. Window dimension measurement

A small demo

Display a floating window through windowmanager.addview (XX):

private void showView() {
        //Get WindowManager instance
        wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);

        //Set the layoutparams property
        layoutParams = new WindowManager.LayoutParams();
        //Width height dimension
        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
        layoutParams.format = PixelFormat.TRANSPARENT;
        //Set background shadow
        layoutParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        layoutParams.dimAmount = 0.6f;

        //Window type
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }

        //Construct textview
        TextView myView = new TextView(this);
        myView.setText("hello window");
        //Set the background to red
        myView.setBackgroundResource(R.color.colorRed);
        FrameLayout.LayoutParams myParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 400);
        myParam.gravity = Gravity.CENTER;
        myView.setLayoutParams(myParam);

        //Myframelayout as rootview
        FrameLayout myFrameLayout = new FrameLayout(this);
        //Set the background to green
        myFrameLayout.setBackgroundColor(Color.GREEN);
        myFrameLayout.addView(myView);

        //Add to window
        wm.addView(myFrameLayout, layoutParams);
    }

The above codes are briefly summarized as follows:

1. Construct textview and set its background to red
2. Construct the FrameLayout and set its background to green
3. Add textview as a child view to FrameLayout
4. Add FrameLayout to the window as a rootview

The suspended window shows the complete demo. Please move to:Window / WindowManager must know

be aware

wm.addView(myFrameLayout, layoutParams);

In layoutparams, we focus on the values of width and height fields. We know that this is a size constraint for window. Take width as an example, set different values to see the effect:
1、wrap_content

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

How does Android window determine the reason why / onmeasure() is executed multiple times

It can be seen that the rootview (FrameLayout) is wrapped around the textview with the same width.

2、match_parent

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;

How does Android window determine the reason why / onmeasure() is executed multiple times

It can be seen that the rootview (FrameLayout) is wide and fills the screen

3. Set specific values

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = 800;

How does Android window determine the reason why / onmeasure() is executed multiple times

It can be seen that the width of the rootview (FrameLayout) does not fill the screen, and the screen width is 1080px.

Combined with the above three figures, we have reason to believe that the layoutparams in WM. Addview (myframelayout, layoutparams) is used to constrain myframelayout (rootview). How does the window size come from?

Determination of window size

Starting from WM. Addview (XX), WindowManager is an interface, and its implementation class is windowmanagerimpl.

#WindowManagerImpl.java
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        //Assign a token, and the value will be judged when starting dialog / popupdialog
        applyDefaultToken(params);
        //Mglobal is a single example to manage all viewrootimpl and rootview
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

Next, look at the processing of windowmanagerglobal:

#WindowManagerGlobal.java
    public void addView(View view, ViewGroup.LayoutParams params,
                        Display display, Window parentWindow) {
        ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...
            //Construct viewrootimpl object
            root = new ViewRootImpl(view.getContext(), display);

            //View as rootview
            //Use the passed wparams as the layoutparams of the rootview
            view.setLayoutParams(wparams);

            //Record object
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            try {
                //Viewrootimpl Association rootview
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                ...
            }
        }
    }

It can be seen from the above that the layoutparams passed in WM. Addview (XX) is set to rootview.
Continue with the viewrootimpl. Setview (XX) process.

#ViewRootImpl.java
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ...
                //Record layoutparams into the member variable mwindow attributes
                //This variable describes the window property
                mWindowAttributes.copyFrom(attrs);
                ...
                //Open the three processes of view layout
                requestLayout();
                ...
                try {
                    ...
                    //IPC communication, tell windowmanagerservice to create a window
                    //Pass mwindow attributes into
                    //The returned mtmpframe represents the maximum size that the window can display
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
                    //Record the returned value into the member variable mwinframe
                    setFrame(mTmpFrame);
                } catch (RemoteException e) {
                    ...
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
                ...
            }
        }
    }

The above paragraph focuses on two aspects:

1. The passed layoutparams is recorded in the member variable mwindowattributes, which is finally used to constrain window.
2. When adding a window, the maximum size of the window is returned and finally recorded in the member variable: mwiframe.

To sum up, we find that:
Layoutparams in WM. Addview (myframelayout, layoutparams) constrains not only rootview but also window.

2. Root dimension measurement

Now that we know the layoutparams of rootview, according to the measurement process of viewtree we analyzed earlier:Measure process of Android custom view
It can be seen that the measurespec object needs to be generated for rootview.
In the setView (XX) process, the callback is invoked by requestLayout, and performTraversals () is executed when the screen refresh signal arrives. The three processes are opened.

#ViewRootImpl.java
    private void performTraversals() {
        ...
        //Previously recorded window layoutparams
        WindowManager.LayoutParams lp = mWindowAttributes;

        //Window size required
        int desiredWindowWidth;
        int desiredWindowHeight;
        ...

        Rect frame = mWinFrame;
        if (mFirst) {
            ...
            if (shouldUseDisplaySize(lp)) {
                ...
            } else {
                //Mwinframe is the maximum window size returned when adding a window previously
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }
            ...
        } else {
            ...
        }

        ...
        if (layoutRequested) {
            ...
            //From the method name, it should be measurement viewtree ----------- (1)
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }
        ...

        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            ...
            try {
                ...
                //Redetermine window size (2)
                relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
                ...
            } catch (RemoteException e) {
            }
            ...
            if (!mStopped || mReportNextDraw) {
                ...
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    ...
                    //Measure viewtree again (3)
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    ...
                }
            }
        } else {
            ...
        }
        ...
        if (didLayout) {
            //Layout viewtree ------------ (4)
            performLayout(lp, mWidth, mHeight);
            ...
        }
        ...
        if (!cancelDraw) {
            ...
            //Start viewtree draw process ---- (5)
            performDraw();
        } else {
            ...
        }
    }

Let’s take a look at the key points of the annotation:
(1)
measureHierarchy(xx)

#ViewRootImpl.java
    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                     final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
        ...

        //Mark whether the measurement is successful
        boolean goodMeasure = false;
        //Wrap width_ 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) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            //Basesize is the preset width
            //Is the desired width of desiredwindownwidth greater than the preset width
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                //Pass in basesize as width
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //Measurement ------------ first time
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                //If the width required by the child layout of the viewtree is greater than that given by the parent layout, this flag is set
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    //This flag is not set, indicating that the size given by the parent layout is sufficient and the measurement is completed
                    goodMeasure = true;
                } else {
                    //The parent layout cannot meet the needs of the child layout. Try to expand the width
                    //Desiredwindownwidth > basesize, so the newly calculated basesize is larger than the original basesize
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    //Continue to measure after receiving ------------- the second time
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    //Continue to check whether it meets the requirements
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        goodMeasure = true;
                    }
                }
            }
        }

        //Not measured well, continue to measure
        if (!goodMeasure) {
            //It can be seen that measurespec is generated for rootview
            //Parameters passed in: the maximum size value that can be assigned to rootview and the desired size of rootview itself (recorded in layoutparams)
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

            //Now that measurespec is available, you can measure rootview
            //This process is the third time to measure the entire viewtree
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                //The window size has changed, which is used for subsequent judgment and execution of performmeasure (XX)
                windowSizeMayChange = true;
            }
        }
        ...
        return windowSizeMayChange;
    }

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

            case ViewGroup.LayoutParams.MATCH_PARENT:
                //If rootview wants to fill the window, it is satisfied, and its size is the exact value
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                //If rootview wants to determine the size according to its own content, it is set to at_ Most mode
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                //If rootview wants to specify the dimension value directly, it is satisfied, and its dimension is the exact value
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
    }

The above code mainly does two things:

1. Determine the measurement mode and estimated measurement value (measurespec) of rootview in combination with the size of window
2. According to the results of the first step, initiate the measurement of viewtree (starting from rootview)

(2)
After measuring the viewtree, the measured value of rootview has been determined.
Take a look at relayoutwindow (XX):

#ViewRootImpl.java
    private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
                               boolean insetsPending) throws RemoteException {
        ...
        //Resize window
        //The dimension value passed in is the dimension value of rootview
        //The returned window size value is stored in mtmpframe
        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,
                mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
        //Associate window and surface
        if (mSurfaceControl.isValid()) {
            mSurface.copyFrom(mSurfaceControl);
        } else {
            destroySurface();
        }

        //Record window size
        setFrame(mTmpFrame);
        return relayoutResult;
    }

We found that:

  • The size of window depends on the measured size of rootview, and generally appscale = 1, that is, the size of window is the size of rootview.
  • This explains the previous demo phenomenon.

The measurement of viewtree in step (1) is to determine the size of rootview, so as to determine the size of window in this step.

(3)(4)(5)

Here is the classic question: why does onmeasure() execute multiple times?

These three parts are the three well-known processes of view. It is worth noting here:
Step (1)
In the measurement hierarchy (XX) in step (1), we marked three measurements.

1. The first time: measure the viewtree with the preset width to get the measurement result
2. It is found that the first measurement result is not satisfied, because there is a sub layout with a required width greater than the preset width, so give a larger width to the sub layout and carry out the second measurement
3. It is found that the second measurement result is still not satisfied, so it is measured again with the maximum width that window can get

It can be seen that performmeasure () is executed at least once in measurehierarchy (XX), and can be executed three times at most. Call the performmeasure () method, which will eventually call onmeasure () of each view.
Can performmeasure() always trigger the execution of onmeasure()? meeting.
reason:
Briefly review the measure (XX) code again:

public final void measure(xx) {
        ...
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        //One of the two conditions is satisfied
        //1. Forced layout required
        //2. Size change
        if (forceLayout || needsLayout) {
            ...
            onMeasure();
            ...
        }
        ...
    }

It can be seen from the above that onmeasure() can be executed, indicating that the above conditions are met.
Needslayout is definitely not satisfied, because the view size has not changed.
Then it can only be forcelayout = true. When measuring the viewtree for the first time, it only went through the measure process, not the layout process. And we know that pflag_ FORCE_ The layout tag is cleared after the layout, so pflag is used here_ FORCE_ The layout tag is not cleared. Of course, needslayout = true satisfies the condition.
For detailed measure / layout / draw series, please move to:

Step (3)
In step (3), the viewtree is measured again, and view / ViewGroup onmeasure() is executed again.

In combination with step (1) and step (3), it is summarized as follows:

At least one measurement is performed in step (1) and up to three measurements are performed
A measurement will also be performed in step (3)

When requestlayout, step (1) must be executed, that is, perform measure (XX) – > onmeasure () at least once.
Step (3) will be executed when the view is displayed for the first time or when the window size changes.
So we come to the following conclusion:

1. When the view is displayed for the first time, steps (1) and (3) must be executed, so onmeasure () must be executed at least twice
2. Step (3) is not necessarily executed when it is triggered by requestlayout(), so onmeasure() may only be executed once

This is why onmeasure () executes multiple times
When will step (1) perform three measurements?
Generally speaking, in step (1), only the third measurement will be taken, and the first and second measurements will not be taken, because for decorview as a rootview, lp.width = = viewgroup.layoutparams.match_ PARENT。 Not meeting the conditions for the first and second time. So you usually only see onmeasure () execute twice, not multiple times.
Of course, we can satisfy its condition and let onmeasure () execute five times.

#Display suspension window
    private void testMeasure() {
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        //It must be wrap_ CONTENT
        layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

        //Transview is a custom view
        final TransView transView = new TransView(this);
        windowManager.addView(transView, layoutParams);
    }

    #Override transview onmeasure (XX)
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //The view needs to be wider than the screen width
        int width = resolveSizeAndState(10000, widthMeasureSpec, 0);
        setMeasuredDimension(width, View.MeasureSpec.getSize(heightMeasureSpec));
    }

Transview is a custom view and transview is a rootview.
The above code mainly does two things:

1. Constrains the width of the window to wrap_ CONTENT
2. Let its child view (transview) request a width greater than the screen width

You can print in transview onmeasure (XX) how many times you want to verify.

In addition, in addition to the above reasons, onmeasure() may be executed more times. For example, when FrameLayout measures the sub layout, the child. Measure() process will be triggered again under some conditions. At this time, the onmeasure() execution times of the sub layout may be more. If you are interested, you can see FrameLayout – > onlayout (XX).

Why does onmeasure() execute twice

We know the reason why it will be executed twice. Why is it so designed?
Regardless of special circumstances, the view will execute onmeasure (XX) twice during the first presentation.
As mentioned earlier, as long as requestlayout () is executed, step (1) will be executed.
The purpose of step (1) is to obtain the measured value of rootview. The measured value of rootview will be used to re determine the width and height of the window in relaywindow (XX), while step (3) is executed after relaywindow (XX). Therefore, it is necessary to execute step (1).

So far, we know how to determine the dimensions of rootview and window.

3. Relationship among window, viewrootimpl and view

The above involves window and rootview, and the methods used are basically provided in viewrootimpl. So what is the relationship between the three?
The rootview needs to be added to the window to be displayed, but the window does not directly manage the rootview, but through viewrootimpl.

How does Android window determine the reason why / onmeasure() is executed multiple times

Viewrootimpl can be regarded as the intermediary between window and rootview, and is responsible for coordinating the two.

What is the relationship between window and rootview?
You may have found that addtodisplay (XX) does not pass in the rootview. How is the rootview added to the window?
In fact, the addition here is more anthropomorphic.
In the relayoutwindow (XX), the mssurfacecontrol is passed in, and the contact with the surface mssurface is established after returning. That is, the surface of the underlying layer is associated with the surface of the Java layer. Through the surface, you can get the canvas. The painting of each view needs to be associated with canvas, and so on. The view is associated with the surface, so the painting on the view will be fed back to the surface. This means that the view is added to the window.

How does Android window determine the reason why / onmeasure() is executed multiple times

This article is based on Android 10.0

Author: fishforest
Link:https://www.jianshu.com/p/6e45f42da304
Source: developeppaper
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.