onMeasure()が複数回実行されるのはなぜですか?Androidは長い間困惑した質問です!

序文

親レイアウトは、それ自体と子レイアウトの要件に従って子レイアウトの測定モードと測定サイズを生成し、それをMeasureSpecオブジェクトにカプセル化し、最後に子レイアウトに渡して最終的に決定できるようにします。自分のサイズ。
子レイアウトは親レイアウトから取得した測定結果であるため、親レイアウトは親レイアウトから測定結果を取得し、最後にそれを測定したViewTreeの頂点ルートビューに到達すると考えるのは自然なことです。
この質問に従って、ソースコードの観点から調べてください。

この記事を通して、あなたは学びます:

1.ウィンドウサイズの測定。
2.ルートビューサイズの測定。
3. Window、ViewRootImpl、およびViewの間の関係。

1.ウィンドウサイズの測定

小さなデモ

WindowManager.addView(xx)を介してフローティングウィンドウを表示します。

private void showView() {

    //获取WindowManager实例
    wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);

    //设置LayoutParams属性
    layoutParams = new WindowManager.LayoutParams();
    //宽高尺寸
    layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
    layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
    layoutParams.format = PixelFormat.TRANSPARENT;
    //设置背景阴暗
    layoutParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
    layoutParams.dimAmount = 0.6f;

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

    //构造TextView
    TextView myView = new TextView(this);
    myView.setText("hello window");
    //设置背景为红色
    myView.setBackgroundResource(R.color.colorRed);
    FrameLayout.LayoutParams myParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 400);
    myParam.gravity = Gravity.CENTER;
    myView.setLayoutParams(myParam);

    //myFrameLayout 作为rootView
    FrameLayout myFrameLayout = new FrameLayout(this);
    //设置背景为绿色
    myFrameLayout.setBackgroundColor(Color.GREEN);
    myFrameLayout.addView(myView);

    //添加到window
    wm.addView(myFrameLayout, layoutParams);
}

上記のコードの概要は次のとおりです。

1. TextViewを作成し、その背景を赤に設定します。
2. FrameLayoutを作成し、その背景を緑に設定します。
3.TextViewを子ビューとしてFrameLayoutに追加します。
4. FrameLayoutをRootView(ルートビュー)としてウィンドウに追加します。

注意:

wm.addView(myFrameLayout, layoutParams);

LayoutParamsは、widthフィールドとheightフィールドの値に焦点を当てています。これがWindowのサイズ制約であることがわかっています。例としてwidthを取り上げます。効果を確認するには、さまざまな値を設定し
ます。1。wrap_content

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;

layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

[画像のアップロードに失敗しました...(image-f8d0f1-1616851800488)]

RootView(FrameLayout)はTextViewをラップし、両方とも同じ幅を持っていることがわかります。

2、match_parent

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;

layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;

次のことがわかります。RootView(FrameLayout)は、画面全体に表示されます。

3.特定の値を設定します

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;

layoutParams.width = 800;

RootView(FrameLayout)の幅が画面全体に表示されておらず、画面の幅が1080pxであることがわかります。

上記の3つの画像を組み合わせると、wm.addView(myFrameLayout、layoutParams)のlayoutParamsがmyFrameLayout(RootView)を制約するために使用されると考える理由があります。では、ウィンドウサイズはどのようになっているのでしょうか。

ウィンドウサイズの決定

wm.addView(xx)の分析によると、WindowManagerはインターフェイスであり、その実装クラスはWindowManagerImplです。

#WindowManagerImpl.java

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    //赋值token,在启动Dialog/PopupDialog 会判断该值
    applyDefaultToken(params);
    //mGlobal 为单例,管理所有的ViewRootImpl、RootView
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

次に、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) {
        ...
        //构造ViewRootImpl 对象
        root = new ViewRootImpl(view.getContext(), display);

        //view 作为RootView
        //将传进来的wparams作为该RootView的LayoutParams
        view.setLayoutParams(wparams);

        //记录对象
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try {
            //ViewRootImpl 关联RootView
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            ...
        }
    }
}

上記から、wm.addView(xx)で渡されたLayoutParamsがRootViewに設定されていることがわかります。
ViewRootImpl.setView(xx)プロセスを引き続き確認します。

#ViewRootImpl.java

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            ...
            //将LayoutParams记录到成员变量 mWindowAttributes 李
            //该变量用来描述Window属性
            mWindowAttributes.copyFrom(attrs);
            ...
            //开启View layout 三大流程
            requestLayout();
            ...
            try {
                ...
                //IPC 通信,告诉WindowManagerService 要创建Window
                //将mWindowAttributes 传入
                //返回mTmpFrame 表示该Window可以展示的最大尺寸
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                        mTempInsets);
                //将返回的值记录到成员变量 mWinFrame 李
                setFrame(mTmpFrame);
            } catch (RemoteException e) {
                ...
            } finally {
                if (restore) {
                    attrs.restore();
                }
            }
            ...
        }
    }
}

上記の段落は2つの側面に焦点を当てています:

1.着信LayoutParamsはメンバー変数mWindowAttributesに記録され、最後にウィンドウを制約するために使用されます。

2.ウィンドウを追加するときにウィンドウの最大サイズを返します。これは最終的にメンバー変数mWinFrameに記録されます。

要約すると、次のことがわかりました。

wm.addView(myFrameLayout、layoutParams)のlayoutParamsは、RootViewを制限するだけでなく、Windowも制限します。

2.ルートビューサイズの測定

RootViewのlayoutParamsがわかったので、以前に分析したViewTreeの測定プロセスに従って:AndroidカスタムViewの測定プロセス

RootView用にMeasureSpecオブジェクトを生成する必要があることがわかります。
setView(xx)のプロセスでは、コールバックを登録するためにrequestLayoutが呼び出されます。画面更新シグナルが到着すると、performTraversals()が実行され、3つの主要なプロセスが開始されます。

#ViewRootImpl.java

private void performTraversals() {
    ...
    //之前记录的Window LayoutParams
    WindowManager.LayoutParams lp = mWindowAttributes;

    //Window需要的大小
    int desiredWindowWidth;
    int desiredWindowHeight;
    ...

    Rect frame = mWinFrame;
    if (mFirst) {
        ...
        if (shouldUseDisplaySize(lp)) {
            ...
        } else {
            //mWinFrame即是之前添加Window时返回的Window最大尺寸
            desiredWindowWidth = mWinFrame.width();
            desiredWindowHeight = mWinFrame.height();
        }
        ...
    } else {
        ...
    }

    ...
    if (layoutRequested) {
        ...
        //从方法名看应该是测量ViewTree -----------(1)
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
    }
    ...

    if (mFirst || windowShouldResize || insetsChanged ||
            viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
        ...
        try {
            ...
            //重新确定Window尺寸 --------(2)
            relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
            ...
        } catch (RemoteException e) {
        }
        ...
        if (!mStopped || mReportNextDraw) {
            ...
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                    updatedConfiguration) {
                ...
                //再次测量ViewTree -------- (3)
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ...
            }
        }
    } else {
        ...
    }
    ...
    if (didLayout) {
        //对ViewTree 进行Layout ---------- (4)
        performLayout(lp, mWidth, mHeight);
        ...
    }
    ...
    if (!cancelDraw) {
        ...
        //开始ViewTree Draw过程 ------- (5)
        performDraw();
    } else {
        ...
    }
}

ラベルの要点を見てみましょう:
(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;
    ...

    //标记是否测量成功
    boolean goodMeasure = false;
    //宽度为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) {
            baseSize = (int)mTmpValue.getDimension(packageMetrics);
        }
        //baseSize 为预置的宽度
        //desiredWindowWidth 想要的宽度是否大于预置宽度
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            //以baseSize 作为宽度传入
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            //测量----------------- 第一次
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            //如果ViewTree的子布局需要的宽度大于父布局能给的宽度,则该标记被设置
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                //该标记没被设置,说明父布局给的尺寸够用,测量完成
                goodMeasure = true;
            } else {
                //父布局不能满足子布局的需求,尝试扩大宽度
                //desiredWindowWidth > baseSize,因此新计算的baseSize要大于原先的baseSize
                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) {
        //可以看出是为RootView 生成MeasureSpec
        //传入的参数:能给RootView分配的最大尺寸值以及RootView本身想要的尺寸(记录在LayoutParams里)
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

        //既然MeasureSpec 有了,那么就可以测量RootView了
        //该过程就是测量整个ViewTree----------------- 第三次
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            //Window尺寸变化了,用于后续判断执行performMeasure(xx)
            windowSizeMayChange = true;
        }
    }
    ...
    return windowSizeMayChange;
}

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

        case ViewGroup.LayoutParams.MATCH_PARENT:
            //RootView 希望填充Window,则满足它,此时它尺寸是确切值
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            //RootView 希望根据自身内容来确定尺寸,则设置为AT_MOST 模式
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            //RootView 希望直接指定尺寸值,则满足它,此时它尺寸是确切值
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

上記のコードは主に2つのことを行います。

1.ウィンドウサイズに基づいて、RootView測定モードと推定測定値(MeasureSpec)を決定します。

2.最初のステップの結果に従って、ViewTreeの測定を開始します(RootViewから開始)。

(2)ViewTreeの測定後、RootViewの測定値が決定されています。

RelayoutWindow(xx)を見てください:

#ViewRootImpl.java

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
                           boolean insetsPending) throws RemoteException {
    ...
    //重新设置Window大小
    //传入的尺寸值为RootView的尺寸值
    //返回Window尺寸值存放在 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);
    //关联Window和surface
    if (mSurfaceControl.isValid()) {
        mSurface.copyFrom(mSurfaceControl);
    } else {
        destroySurface();
    }

    //记录Window 尺寸
    setFrame(mTmpFrame);
    return relayoutResult;
}

私たちは発見します:

ウィンドウのサイズは、測定されたRootViewのサイズに依存し、一般的にはappScale = 1です。これは、ウィンドウのサイズがRootViewのサイズであることを意味します。

これは、以前のデモ現象の説明でもあります。

そして、(1)ViewTreeを測定するステップは、このステップでWindowのサイズを決定するようにRootViewのサイズを決定することです。

これが古典的な質問の分析です:なぜonMeasure()が複数回実行されるのですか?

これらの3つの部分は、私たちが精通しているビューの3つのプロセスです。さらに、次の点に注意してください。
ステップ(1):ステップ(1)
のmeasureHierarchy(xx)で、3つの測定値をマークしました。

1.初回:最初にプリセット幅でViewTreeを測定し、測定結果を取得します。

2.事前に設定した幅よりも広い幅が必要なサブレイアウトがあるため、最初の測定結果では不十分であることがわかり、サブレイアウトに広い幅を与えて2回目の測定を行います。

3. 2番目の測定結果はまだ満足のいくものではないことがわかったので、Windowが再度測定できる最大幅を使用します。

performMeasure()はmeasureHierarchy(xx)で少なくとも1回実行され、最大3回実行できることがわかります。performMeasure()メソッドを呼び出します。これにより、最終的に各ビューのonMeasure()が呼び出されます。
performMeasure()は常にonMeasure()の実行をトリガーしますか?会議。
理由:もう一度、measure(xx)コードを簡単に確認してください。

public final void measure(xx) {

    ...
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    //两个条件满足其中一个
    //1、需要强制layout
    //2、尺寸发生改变
    if (forceLayout || needsLayout) {
        ...
        onMeasure();
        ...
    }
    ...
}

上記から、onMeasure()を実行できることがわかり、上記の条件が満たされていることがわかります。

ビューサイズが変更されていないため、needsLayoutは完全に満たされていません。
その場合、forceLayout = trueのみになります。ViewTreeが初めて測定されたときは、レイアウトプロセスではなく、測定プロセスのみが実行されました。また、レイアウトの終了後にPFLAG_FORCE_LAYOUTマークがクリアされることがわかっているため、ここではPFLAG_FORCE_LAYOUTマークはクリアされません。もちろん、needsLayout = trueは条件を満たしています。

ステップ(3):ステップ(3)で
、ViewTreeが再度測定され、この時点でView / ViewGroup onMeasure()が再度実行されます。

手順(1)と(3)を組み合わせて、以下を要約します。

ステップ(1)で少なくとも1回の測定が実行され、最大3回の測定が実行されました。

ステップ(3)では、測定も実行されます。

requestLayoutの場合、ステップ(1)が実行されます。つまり、performMeasure(xx)-> onMeasure()が1回実行されます。
また、ビューが初めて表示されたとき、またはウィンドウサイズが変更されたときに、手順(3)が実行されます。
したがって、次の結論に達します。

1.ビューを初めて表示するときは、手順(1)と(3)を実行する必要があるため、onMeasure()は少なくとも2回実行されます。

2. requestLayout()によってトリガーされた場合、ステップ(3)は必ずしも実行されるとは限らないため、onMeasure()はこの時点で1回しか実行できません。

これが、onMeasure()が複数回実行される理由です。
ステップ(1)はいつ3つの測定を実行しますか?
一般的に、ステップ(1)では、3番目の測定のみが行われ、1番目と2番目の測定は行われません。これは、DecorViewがRootViewである場合、lp.width == ViewGroup.LayoutParams.MATCH_PARENTであるためです。1回目と2回目の歩行の条件を満たさない。したがって、通常、onMeasure()は2回だけ実行され、複数回実行されることはありません。
もちろん、その条件を満たすことができ、onMeasure()を5回実行させることができます。

#展示悬浮窗

private void testMeasure() {
    WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
    layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
    //一定要是WRAP_CONTENT
    layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

    //TransView为自定义View
    final TransView transView = new TransView(this);
    windowManager.addView(transView, layoutParams);
}

#重写TransView onMeasure(xx)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //该View 需要宽度大于屏幕宽度
    int width = resolveSizeAndState(10000, widthMeasureSpec, 0);
    setMeasuredDimension(width, View.MeasureSpec.getSize(heightMeasureSpec));
}

TransViewはカスタムビューであり、TransViewはRootViewです。
上記のコードは主に2つのことを行います。

1.ウィンドウの幅をWRAP_CONTENTに制限します。

2.子ビュー(TransView)を画面幅よりも広い幅に適用します。

実行回数を確認するには、TransView onMeasure(xx)に出力するだけです。

さらに、onMeasure()が複数回実行される上記の理由に加えて、onMeasure()がより多く実行される場合があります。たとえば、FrameLayoutがサブレイアウトを測定している場合、child.measure()プロセスは次のようになります。特定の条件下で再度トリガーされます。カウントアップすると、サブレイアウトのonMeasure()実行の数が増える可能性があります。興味がある場合は、FrameLayout-> onLayout(xx)を参照してください。

onMeasure()が2回実行されるのはなぜですか

2回実行される理由はわかっていますが、なぜこのように設計されているのでしょうか。
特別な状況に関係なく、Viewは最初に表示されたときにonMeasure(xx)を2回実行します。
前述のように、requestLayout()が実行されている限り、ステップ(1)は確実に実行されます。
ステップ(1)の目的はRootViewの測定値を取得することであり、RootViewの測定値はrelayoutWindow(xx)で使用されてウィンドウの幅と高さを再決定し、ステップ(3)はrelayoutWindowの後に実行されます。 (xx)なので、実行にはステップ(1)が必要です。

これまでのところ、RootViewとWindowのサイズを決定する方法を知っています。

3. Window、ViewRootImpl、およびViewの関係

上記にはWindowとRootViewが含まれ、使用されるメソッドは基本的にViewRootImplで提供されるものです。では、3つの関係は何ですか?

表示するにはRootViewをWindowに追加する必要がありますが、WindowはRootViewを直接管理するのではなく、ViewRootImplを介して管理します。

ViewRootImplは、WindowとRootViewの間の仲介者と見なすことができ、2つの調整を担当します。

では、WindowとRootViewの関係は何ですか?
addToDisplay(xx)がRootViewに渡されないことに気付いたかもしれませんが、RootViewはどのようにWindowに追加されますか?
実際、ここでの追加はより擬人化されています。
RelayoutWindow(xx)では、mSurfaceControlが渡され、戻った後、SurfacemSurfaceとの接続が確立されます。つまり、最下層の表面はJava層の表面に関連付けられています。そして、Surfaceを通じてCanvasを入手できます。各ビューの描画はキャンバスに関連付ける必要があります。以下同様に、ビューはサーフェスに関連付けられ、ビューの描画はサーフェスにフィードバックされます。これは、ビューがウィンドウに追加されることを意味します。

この記事はAndroid10.0に基づいています

おすすめ

転載: blog.csdn.net/A_pyf/article/details/115272336