从performTraversals到三大流程的调用流程

这篇主要想梳理一下真实的一个Demo它的measure、layout、draw到底是怎么样的一个调用过程。

前几天,我在嵌套在RelativeLayout中的自定义View的onMeasure()、onLayout()、onDraw()中打印信息,居然输出了四次measure两次layout,有一部分是因为RelativeLayout两次测量子View。另外就是只知道performTraversals()是三大流程的起点,但是不知道到底是怎么样调用的,所以.....

开始之前应该先复习下面这些基础知识。

一、调用过程的结论

API26从performTraversals()到三大流程的运行步骤如下。

第一次performTraversals()

  1. 第一个measureHierarchy()被调用。
    • performMeasure()被调用。
    • View的measure()被调用。
    • DecorView的onMeasure()被调用,递归到底。(onMeasure第一次)
  2. 第一个performMeasure()被调用。
    • View的measure()被调用。
    • DecorView的onMeasure()被调用,递归到底。(onMeasure第二次)
  3. performLayout()被调用。
    • layout()被调用
    • onLayout()被调用,递归到底。(onLayout第一次)
  4. performDraw()没有被调用,开始了新一轮的performTraversals()。

第二次performTraversals()

  1. 第一个measureHierarchy()被调用。
    • performMeasure()被调用。
    • View的measure()被调用。
    • 内层自定义View的onMeasure()没有被调用。
  2. performLayout()被调用。
    • layout()被调用。
    • 内层自定义View的onLayout()没有被调用。
  3. performDraw()被调用。
    • draw()被调用,onDraw()被调用,递归到底。(onDraw第一次)

二、DecorView结构

测试的布局

Activtiy加载以下布局文件,其中Circle为继承View的自定义View。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.utte.viewevent.CircleView
        android:layout_width="400dp"
        android:layout_height="200dp" />

</LinearLayout>

使用的style是默认项目的AppTheme。

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

DecorView结构

如果使用上面的测试布局,DecorView在第一次测量时将会是如下结构:

DecorView(3)

  1. LinearLayout(2)
    1. ViewStub (GONE)
    2. FrameLayout(1)
      1. ActionBarOverlayLayout(2)
        1. ActionBarContainer(2)
          • ToolBar(1)
            • AppCompatTextView
          • ActionBarContextView(GONE)
        2. ContentFrameLayout(1, 这里是Activity加载的布局)
          • LinearLayout(1)
            • 自定义的CircleView
  2. View(id为navigationBarBackgroud)
  3. View(id为statusBarBackgroud)

注意

  1. ContentFrameLayout中的布局就是Activity中setContentView加载进去的布局。
  2. 这个DecorView并不是不变的,在后面的measure、layout过程中,可能会增加布局。

三、详细流程--第一次performTraversals()

1)第一个measureHierarchy()被调用

这是第一次测量,也是步骤最多最复杂的。所以分析得稍微详细些,后面的流程和这个都差不多。

1. measureHierarchy()被调用

调用条件是layoutRequested为true。

boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
    // ......
    windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
}

mLayoutRequested在1629行由于mFirst为true,所以被赋值为true。

if (mFirst) {
    // ......
    mLayoutRequested = true;
    // ......
}

mStopped初始化为false,并且一直为false,没有找到任何地方将它赋值为true。

mReportNextDraw暂时应该没有涉及到,所以默认为false。

最终layoutRequested为true,measureHierarchy()被调用。

2. ViewRootImpl#measureHierarchy() -> DecorView#measure()

在measureHierarchy()中,由于窗口宽不为wrap_content,所以1471和1484的performMeasure()没有被调用,并且goodMasure没有被赋值为true。1498的performMeasure()调用条件为goodMeasure为false,所以这个performMeasure()被调用了。

if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
    // ......
    if (baseSize != 0 && desiredWindowWidth > baseSize) {
        // ......
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        // .......
        if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
            goodMeasure = true;
        } else {
            // ......
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                goodMeasure = true;
            }
        }
    }
}
if (!goodMeasure) {
    // ......
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // ......
}

performMeasure中只要mView(此时为DecorView)不空,就会调用mView的measure()。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    // ......
}

3. DecorView的第一次测量过程

由于只有View实现了measure(),是final的方法,所以所有视图调用measure()都会直接到了View的measure()。调用mView(DecorView)的measure(),实际是调用了View统一实现的measure()。

View#measure() -> DecorView#onMeasure()

在measure()中调用onMeasure的条件是:

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
    // ......
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // ......
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        // ......

sIgnoreMeasureCache值在API20之前一直为true,API20之后一直为false。此处forceLayout为true,所以onMeasure()被调用。

DecorView的测量分发

到达DecorView的onMeasure(),里面直接调用了其父类(FrameLayout)的onMeasure()。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ......
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // ......
}

到达FrameLayout的onMeasure(),首先计算DecorView的子View个数,三个,一个LinearLayout和两个普通的View,分别对他们调用measureChildWithMargins(),measureChildWithMargins()会调用子View的measure()。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // ......
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // ......
        }
    }
    // ......
}
a. DecorView第一个子View--LinearLayout的测量

measureChildWithMargins()调用了LinearLayout的measure()(其实就是View的measure())。

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // ......
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

这个measure()的forceLayout是ture,所以调用到LinearLayout的onMeasure()。进入了LinearLayout的onMeasure()后,判断了orientation,进入了measureVertical(),LinearLayout有两个子View。

i. LinearLayout的第一个子View--ViewStub

第一个是ViewStub,但是这个ViewStub的状态是GONE的,于是没有做测量的工作。

ii. LinearLayout的第二个子View--FrameLayout

第二个子View是一个FrameLayout,一直调用到FrameLayout的onMeasure()。在onMeasure中得出FrameLayout子View只有一个,ActionBarOverlayLayout。

ActionBarOverlayLayout的onMeasure()被调用,测量过程会向两个子View传递,ActionBarContainer和ContentFrameLayout。

ActionBarContainer
  1. ActionBarOverlayLayout对mActionBarTop调用了measureChildWithMargins(),mActionBarTop就是ActionBarContainer。ActionBarContainer的onMeasure()被调用,ActionBarContainer继承了FrameLayout(),在它的onMeasure()中调用了父类(FrameLayout)的onMeasre()。

  2. ActionBarContainer还有两个子View,一个是Toolbar,Toolbar的onMeasure()被调用,Toolbar里面还有一个子View,AppCompatTextView,这个TextView的绘制过程就不说了。

  3. 接着走到了ActionBarContainer的第二个子View,是ActionBarContextView,它是GONE的状态。ActionBarContainer的两个子View就遍历完了。

ContentFrameLayout
  1. 接着ActionBarOverlayLayout对mContent调用measureChildWithMargins(),mContent是ContentFrameLayout,调用到ContentFrameLayout的onMeasure(),调用了它父类(FrameLayout)的onMeasure()。ContentFrameLayout有一个子View,是LinearLayout。

  2. 这个LinearLayout就是前面代码中我们自己定义的布局文件中的LinearLayout。进入measureVertical(),里面一个子View,是我自定义的CircleView。CircleView的onMeasure()被调用。ContentFrameLayout测量结束。

b. DecorView第二、三个子View的测量

后面两个子View都是普通的View,一个id为navigationBarBackgroud,一个为statusBarBackgroud。至此,DecorView的测量就完成了,回到了ViewRootImp()的performTraversals()。

2)第二个measureHierarchy()没有被调用

mApplyInsetsRequested为false。

if (mApplyInsetsRequested) {
    mApplyInsetsRequested = false;
    // ......
    if (mLayoutRequested) {
        // .......
        windowSizeMayChange |= measureHierarchy(host, lp,
                mView.getContext().getResources(),
                desiredWindowWidth, desiredWindowHeight);
    }
}

3)第一个performMeasure()被调用

mStopped为false,updatedConfiguration为true。

if (!mStopped || mReportNextDraw) {
    boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
            (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
    if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
            || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
            updatedConfiguration) {
        // ......
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

DecorView第二次测量流程和第一次一样。

4)第二个performMeasure()未被调用

weight都是0,measureAgain为false。

boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
    // ......
    measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
    // ......
    measureAgain = true;
}
if (measureAgain) {
    // ......
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

5)performLayout()会被调用

调用条件

layoutRequested为true,mStopped为false,mReportNextDraw为false。didLayout为true,所以performLayout被调用。

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
// ......
if (didLayout) {
    performLayout(lp, mWidth, mHeight);
    // ......
}

ViewRootImpl#performLayout()->DecorView#onLayout()

performLayout()中只要mView(DecorView)不空,就会调用到mView的layout(),DecorView没有重写layout(),所以调用到ViewGroup的layout()。ViewGroup调用了父类(View)的layout()。View的layout调用onLayout的条件如下,changed为true,DecorView的onLayout()被调用。

boolean changed = isLayoutModeOptical(mParent) ?
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    onLayout(changed, l, t, r, b);

DecorView布局流程分发

DecorView的onLayout会直接调用父类(FrameLayout)的onLayout()。在这遍历了DecorView的子View,调用每一个子View的layout(),和measure流程类似的分发。

LinearLayout中没有重写layout(),所以调用了ViewGroup的layout()->View的layout()->LinearLayout的onLayout()->LinearLayout的onLayout()->LinearLayout的layoutVertical(),有两个子View,ViewStub还是不存在,之后会对第二个子View FrameLayout调用setChildFrame(),内部调用FrameLayout的layout()。然后调用到FrameLayout的onLayout(),继续分发到底。

和measure有一点点不同的是会先处理ContentFrameLayout,再处理ActionBarContainer。

6)performDraw()未被调用

newSurface为true。isViewVisible为true,performDraw()没有被调用。在这里调用了scheduleTraversals(),所以会有第二次performTraversals()。

if (!cancelDraw && !newSurface) {
    // ......
    performDraw();
} else {
    if (isViewVisible) {
        // Try again
        scheduleTraversals();
    }
    // ......
}

四、详细流程--第二次performTraversals()

1) measureHierarchy()被调用

这里mLayoutRequested为true,mStopped为false,mReportNextDraw为true,因为在上一次performTraversals()时report了一次。

boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
    // ......
    windowSizeMayChange |= measureHierarchy(host, lp, res,
            desiredWindowWidth, desiredWindowHeight);
}

之后一样的goodMeasure为false,performMeasure()被调用。大体是一样的,但是为什么这次测量并没有进入自定义View的onMeasure呢,就是因为在调用ContentFrameLayout的measure()时,forceLayout是false,所以ContentFrameLayout的onMeasure()没有被调用。

另外一个和之前不同的是Toolbar中的onMeasure()会去调用ActionMenuView的onMeasure(),ActionMenuView在之前的两次测量中并没有出现。

2) 第二个measureHierarchy()没被调用

和第一次performTraversals一样的原因。

3) 第一、二个performMeasure()没被调用

mStopped为false,mReportNextDraw为true,但是第二个if没有进入,updatedConfiguration为false。

if (!mStopped || mReportNextDraw) {
    
    if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
            || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
            updatedConfiguration) {
        // .......
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        // .......
        if (measureAgain) {
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
        layoutRequested = true;
    }
}

4) performLayout()被调用

DecorView的其它部分还是有调用onLayout()的。除了到达ContentFrameLayout调用layout()时,在View的layout()中,changed为false,并且没有设置PFLAG_LAYOUT_REQUIRED,所以他的onLayout()没有被调用。

public void layout(int l, int t, int r, int b) {
    // ......
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        // ......
    }
    // ......
}

5) performDraw()被调用

完成完整的draw流程。

五、总结

这样一分析,三大流程确实清楚了不少,最重要的是三大流程不只是几个单独的方法了,明白了调用流程。

没有想到简简单单的一个布局,居然这么复杂,主要还是这现成的theme结构复杂,早知道就换一个没有Toolbar的theme分析了......

表面上确实是performTraversals()调用了两次,并且方法内部很多measure()的调用,但是因为各种判断,让测量和布局的次数远没有写上去的那么多。

能力有限,还没有理解为什么要这样设计,做两次完整测量和一次不完整测量。

当程序员好累啊...我的颈椎...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容