Android View 测量原理 public class DecorView extends FrameLayout implements RootViewSurfaceTaker , WindowCallbacks { }
我们知道 DecorView 是继承自 FrameLayout,而且 FrameLayout 本身也比较简单,所以本文打算从 FrameLayout 的角度来分析 view 的 measure 过程。
View 的根布局 每个 Activity 都要重写 onCreate 方法,通常是调用 setContentView 来设置当前 Activity 的布局。那么每个 Activity 布局是放在了哪里呢?传说中的根布局到底是什么呢? 一起往下看吧。
@Override public void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
进入 setContentView 的方法里面:
public void setContentView (int layoutResID) { getWindow().setContentView(layoutResID); initActionBar(); }
getWindow() 会返回一个继承 Window 的 PhoneWindow(TV 的话,会是 TVWindow),然后执行它的 setContentView(),而如下所示,PhoneWindow 是在 Activity 的 attach() 中通过 makeNewWindow 生成的
final void attach (Context context, ActivityThread aThread, // 此处省去一些代码…… mWindow = PolicyManager.makeNewWindow(this ) ; mWindow.setCallback(this ); mWindow.getLayoutInflater().setPrivateFactory(this ); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } if (info.uiOptions != 0 ) { mWindow.setUiOptions(info.uiOptions); } }
PolicyManager 通过反射得到了 Policy,如下
public final class PolicyManager { private static final String POLICY_IMPL_CLASS_NAME = "com.android.internal.policy.impl.Policy" ; private static final IPolicy sPolicy; static { try { Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME); sPolicy = (IPolicy)policyClass.newInstance(); } catch (ClassNotFoundException ex) { throw new RuntimeException( POLICY_IMPL_CLASS_NAME + " could not be loaded" , ex); } catch (InstantiationException ex) { throw new RuntimeException( POLICY_IMPL_CLASS_NAME + " could not be instantiated" , ex); } catch (IllegalAccessException ex) { throw new RuntimeException( POLICY_IMPL_CLASS_NAME + " could not be instantiated" , ex); } } public static Window makeNewWindow (Context context) { return sPolicy.makeNewWindow(context); } }
在 Policy 中的 makeNewWindow() 直接返回了一个 PhoneWindow
public Window makeNewWindow (Context context) { return new PhoneWindow(context); }
下面就说到了,上面提的 setContentView()
public class PhoneWindow extends Window implements MenuBuilder .Callback { @Override public void setContentView (int layoutResID) { if (mContentParent == null ) { installDecor(); } else { mContentParent.removeAllViews(); } mLayoutInflater.inflate(layoutResID, mContentParent); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } } }
到这里我们可以看出,就是将我们自己传进来的 layoutId,添加到 mContentParent 下面,而这个 mContentParent 是在 installDecor() 里面赋值的,首先会初始化成员变量 DecorView 类的 mDecor,然后调用 generateLayout(),传入 DecorView,来得到 mContentParent。
private void installDecor () { if (mDecor == null ) { mDecor = generateDecor(); } if (mContentParent == null ) { mContentParent = generateLayout(mDecor); } } protected DecorView generateDecor (int featureId) { return new DecorView(context, featureId, this , getAttributes()); } protected ViewGroup generateLayout (DecorView decor) { ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); }
在 generateLayout() 中,会根据不同的 Style 类型来选择不同的布局文件,然后会 add 进 DecorView 中,然后调用 findViewById() 从 DecorView 里面得到 mContentParent,这个才是真正的根布局,一般情况下,根布局都是 FrameLayout 来担任,我们可以用 xml 的最顶层 viewGroup 调用 getParent(),返回的就是 FrameLayout 对象,其 id 是 android:id=”@android:id/content”。
所以,一个 Activity,里面是 PhoneWindow,然后是 DecorView,这个 DecorView 继承自 FrameLayout,最后,才是我们自己 setContentView 的布局
VSYNC 接下来,View 就显示出来了么?我们熟知的 onMeasure、onLayout 和 onDraw 都还没有调用呢?看起来,我们需要一个时机来触发 View 的操作。 这个就得说下垂直同步机制 (VSYNC):
VSYNC 就是一种同步机制,以某种固定的频率进行同步,当其他组件收到这个同步信号时,就执行相应的操作。设想一下,如果没有这个同步机制,各个模块又怎能知道在哪个时候去执行自己的工作了? 这里可以初步地将 VSYNC 当做闹钟,每间隔固定时间,就响一次,其他组件听到闹铃后,就开始干活了。这个间隔的时间,与屏幕刷新频率有关,例如大多数 Android 设备的刷新频率是 60 FPS(Frame per second),一秒钟刷新 60 次,因而间隔时间就是 1000 / 60 = 16.667 ms。这个时间,大家是不是很熟悉了?看过太多性能优化的文章,都说每一帧的绘制时间不要超过 16 ms,其背后的原因就是这个。绘制每一帧对应的 View,这个步骤发生在 UI 线程上,所以也不要在 UI 线程上进行耗时的操作,否则就可能在 16 ms 内,无法完成界面更新操作了。
长话短说,总结一下,当 Choreographer 接收到 VSYNC 信号后,ViewRootImpl 调用 scheduleTraversals 方法,通知 View 进行相应的渲染,其后 ViewRootImpl 将 View 添加或更新到 Window 上去,并会执行 TraversalRunnable 的 doTraversal(),会调用到 performTraversals(),这是一个非常长的方法,里面就会提到我们熟悉的 Measure、Layout 和 Draw。
measure() 和 onMeasure() performTraversals() 会调用到 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); } }
这里的 mView 就是 DecorView,但 measure() 方法 View 的,并且有 final 修饰,不能复写,在这个方法中调用了 onMeasure()。 在 View 中有默认的 onMeasure 实现:
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } protected int getSuggestedMinimumWidth () { return (mBackground == null ) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } public static int getDefaultSize (int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break ; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break ; } return result; }
getSuggestedMinimumWidth(),如果背景为空,那么我们直接返回 mMinWidth 最小宽度,否则,就在 mMinWidth 和背景最小宽度之间取一个最大值,getSuggestedMinimumHeight 类同,mMinWidth 和 mMinHeight 默认都是 0。
getDefaultSize() 中,当模式为 AT_MOST 和 EXACTLY 时均会返回解算出的测量尺寸,这里的 specSize 是。
FrameLayout 的 onMeasure 方法 @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); int maxHeight = 0 ; int maxWidth = 0 ; int childState = 0 ; for (int i = 0 ; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0 , heightMeasureSpec, 0 ); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); count = mMatchParentChildren.size(); if (count > 1 ) { for (int i = 0 ; i < count; i++) { final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { final int width = Math.max(0 , getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) { final int height = Math.max(0 , getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
measureChildWithMargins 是 ViewGroup 的方法,并且调用了 child.measure 方法。FrameLayout 继承子 ViewGroup
protected void measureChildWithMargins (View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
总结:当 FrameLayout 的宽或高属性为 Wrap_content 属性时,同时有两个及以上的子 view 属性为 match_parent 时,则所有子 view 会 measure 两次
FrameLayout 什么情况下子 view 会 measure 两次? 需要满足两个条件:1.FrameLayout 自身的 MeasureSpec.Mode 不等于 MeasureSpec.EXACTLY。2. 有两个或以上子 view 设置了 match_parent
为什么这种情况 FrameLayout 会测量两次呢?我们先看下面这个问题:
获取 ChildMeasureSpec 时,FrameLayout 与 RelativeLayout 区别 这里还有一个问题:getChildMeasureSpec 是 ViewGroup 的静态方法,但是 getChildMeasureSpec 方法会改变子 view 的 MeasureSpec.Mode 。
如果 FrameLayout 的 SpecMode 是 AT_MOST 或 UNSPECIFIED,而子 view 的 layout 属性时 MATCH_PARENT 或 WRAP_CONTENT,则子 view 的 SpecMode 会变成和父 view 一样
注意看下面代码的注释1
和注释2
public static int getChildMeasureSpec (int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0 , specSize - padding); int resultSize = 0 ; int resultMode = 0 ; switch (specMode) { case MeasureSpec.EXACTLY: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break ; case MeasureSpec.AT_MOST: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break ; case MeasureSpec.UNSPECIFIED: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = 0 ; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = 0 ; resultMode = MeasureSpec.UNSPECIFIED; } break ; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
子ViewMeasureSpec生成规则
回答上面问题,为什么这种情况 FrameLayout 的子 view 会测量两次? 先看下图: 以宽度为例说明,因为 FrameLayout 可能包含多个子 view,第一次测量后,设置了 match_parent 的 view1 和 view2 变为 wrap_content,child.getMeasuredWidth 为 wrap_content 的宽度,即子 view 自己的宽度。 由于有多个 match_parent 的子 view,父 view 需要重新调整自己的宽度为最大的子 view 的宽度 ,所以所有的子 view 需要在根据父 view 的宽度重新调整一下自己的宽度,最终导致子 view measure 两次。
多说一句,RelativeLayout 并没有用这个方法,而是自定义了一个 getChildMeasureSpec() 方法,所以 RelativeLayout 与 FrameLayout 表现上是不一样的。 感兴趣的可以在 Android Studio 里修改一下下面布局,替换 RelativeLayout 和 FrameLayout,试一下看下效果:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_red_light" android:gravity="center" android:text="text" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="2222222222" /> </RelativeLayout>
Ref 测量(Measure)
自定义 View —— onMeasure、 onLayout
本文链接:http://agehua.github.io/2019/12/09/Android-Measure/
------------------------------------------------------------------------------------------------------------------------------
Enjoy it ? Donate me ! 欣赏此文?求鼓励,求支持!
------------------------------------------------------------------------------------------------------------------------------
Enjoy it ? Donate me ! 欣赏此文?求鼓励,求支持!