View的显示过程

7/13/2021 AndroidView源码

本章我们来分析从Activity.setContentview()到Activity.onResume()的源码,先来个总结:

  • 1 setContentView(): 初始化DecorView,并没有添加到Window上
  • 2 onResume()时,Activity并没有添加到屏幕上,onResume()之后才会添加到屏幕上,所以在onResume()里面获取View的宽高不一定能获取到
  • 3 由于本章不涉及Activity的启动过程,我们假设已经创建了Window、Context等前置的东西

不BB,上代码。

# Activity的setContentView():

public void setContentView(int layoutResID) {
    //直接用widnow的setContentView()
    getWindow().setContentView(layoutResID);
    //初始化ActionBar
    initWindowDecorActionBar();
}

//getWindow直接返回mWindow
public Window getWindow() {
    //Window的赋值是PhoneWindow,在Activity的attach里面会创建,我们后面会讲
    return mWindow;
}

//PhoneWindow的setContentView
public void setContentView(int layoutResID) {
    //mContentView是个ViewGroup
    if (mContentParent == null) {
        //1 初始化DecorView
        installDecor();
    }

    //..省略部分代码..
    
    //2 填充布局到mContentParent
    mLayoutInflater.inflate(layoutResID, mContentParent);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

先来看1: installDecor

//我们只看主线代码
private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        //创建DecorView
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {

        //生成mContentParent
        mContentParent = generateLayout(mDecor);

        //**省略部分代码**
    }
}

//DecorView继承自FrameLayout
protected DecorView generateDecor(int featureId) {
    //**省略获取context的代码**

    //直接创建并返回
    return new DecorView(context, featureId, this, getAttributes());
}

//接着来看mContentParent的生成代码,主要逻辑是:根据主题生成不同的ViewGroup并返回,代码很长,只贴出关键部分
protected ViewGroup generateLayout(DecorView decor) {
    //**省略部分代码**
    int layoutResource;
    int features = getLocalFeatures();
    //直接看我们最熟悉的ACTION_BAR相关的
    if((features & (1 << FEATURE_NO_TITLE)) == 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
            layoutResource = a.getResourceId(
                    R.styleable.Window_windowActionBarFullscreenDecorLayout,
                    R.layout.screen_action_bar);
        } else {
            //直接看这里把,指定了一个布局文件(布局文件代码,可以直接在后面看到)
            //这个布局文件中有个android:id = "@android:id/content"的FrameLayout
            layoutResource = R.layout.screen_title;
        }
    }

    //添加这个布局到DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    //findViewById()出刚刚添加的布局里面的id为content的FrameLayout,赋值给contentParent
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }
    
    //返回这个contentParent
    return contentParent;
}

//来看下将布局添加到DecorView的代码(这里简化了代码逻辑,理解即可)
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    //解析这个layoutResource为View
    final View root = inflater.inflate(layoutResource, null);
    //添加到DecorView,这样,DecorView里面就有了个id为content的View了
    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) root;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

R.layout.screen_title的布局文件内容:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />

    <FrameLayout android:id="@android:id/title_container" 
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        android:transitionName="android:title"
        style="?android:attr/windowTitleBackgroundStyle">
    </FrameLayout>
    //这里有个id为content的FrameLayout,也就是setContentView()会添加到这个里面
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

小结一下: 到这里我们可以得出:DecorView是个FrameLayout,里面有个id为content的FrameLayout,就是mContentParent,我们的setContentView()就是添加到这个mContentParent中的,也就是说,Activity默认布局层级至少为2,DecorView套着mContentParent,mContentParent套着我们自己的setContentView()设置的View,当然,不调用setContentView()另说

再来看2: inflate()方法

//LayoutInflater的填充方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

//调用了这个
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    //根据resource对应的xml文件,创建一个XmlResourceParser,准备解析来获取View
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

//又调用到这里
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final Context inflaterContext = mContext;
        //获取AttrbuteSet属性
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        View result = root;

        try {
            //检测是否有START_TAG,没有就抛出异常
            advanceToRootNode(parser);
            final String name = parser.getName();

            //处理merge标签
            if (TAG_MERGE.equals(name)) {
                //这里可以看到一个知识点:<merge>标签的attachRoot必须为true,否则会抛异常
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                //创建View(这里是个知识点,可以进行切入逻辑来操作View)
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                //给View设置LayoutParams
                ViewGroup.LayoutParams params = null;
                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }

                //填充子View,这是一个深度遍历过程
                rInflateChildren(parser, temp, attrs, true);

                //重点!!添加View到root,也就是添加到mContentParent
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        }catch(Exception e){**省略异常处理**}

        //返回mContentParent
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

到此结束,上述代码完事后,就有这么一个结果:

  • 1 入口为Activity的onCreate()里面的setContentView()
  • 2 setContentView(lauoutID)里面的layoutID被解析为一个View
  • 3 这个View被添加到mContentParent了,也就等价于添加到DecorView,注意只是添加到DecorView,没有添加到Window,没有任何其他操作

# ActivityThread的handleResumeActivity

//只贴出部分主线代码
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {
    //取出ActivityClientRecord
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    //取出Activity
    final Activity a = r.activity;
    if (r.window == null && !a.mFinished && willBeVisible) {
        //取出window,如前所属,window就是PhoneWindow,在Activity.atttach()里面创建的
        r.window = r.activity.getWindow();
        //取出DecorView,前面已经创建了
        View decor = r.window.getDecorView();
        //设置为不可见
        decor.setVisibility(View.INVISIBLE);
        //取出WindowManager
        ViewManager wm = a.getWindowManager();
        //取出LayoutParams
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        //Window的类型是:WindowManager.LayoutParams.TYPE_BASE_APPLICATION
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;

        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                //将DecorView添加到Window
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }
    }
    ....
    if (r.activity.mVisibleFromClient) {
        //使得DecorView可见
        r.activity.makeVisible();
    }
    ....
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

上述代码大概为三个步骤:

  • 1 取出DecorView,使得DecorView不可见
  • 2 取出Window,将DecorView添加到Window
  • 3 添加后,使得DecorView可见

来看添加DecorView到Window的代码:wm.addView(decor,l),他的实现在WindowManagerImpl下面:

 @Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    //mGlobal是WindowManagerGlobal
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

//WindowManagerGlobal的addView,只看主线代码
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ....
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    ....
    ViewRootImpl root;
    synchronized (mLock) {
        ....

        //创建ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);

        //设置布局参数
        view.setLayoutParams(wparams);

        //将三个参数存入缓存
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try {
            //将View添加到ViewRootImpl,注意这view是DecorView,也就是最顶层View
            root.setView(view, wparams, null);
        } catch (RuntimeException e) {
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}

//接着看ViewRootImpl.setView(),只贴本次相关的主线代码
 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
         if (mView == null) {
             //保存View
            mView = view;
            ...
            //标记为已添加
            mAdded = true;
            ...
            //触发一次layout,这个方法是这个View体系更新的开端
            requestLayout();
            ...
            //指定DecorView的parent为自己
            view.assignParent(this);
            ...
         }
    }
 }

//View的assignParent()
void assignParent(ViewParent parent) {
    if (mParent == null) {
        //直接保存
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        //如果已经有parent,则直接报错,这就是为什么一个View不能添加到两个ViewGroup的原因
        throw new RuntimeException("view " + this + " being added, but"
                + " it already has a parent");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

小结

  • 1 调用WindowManager.addView(decor),其实现是 WindowManagerImpl.addView(decor)
  • 2 调用了WindowManagerGlobal.addView()
  • 3 最终调用到了ViewRootImpl.setView(),在这里保存了decorView为自己的成员变量,触发了requestLayout(),最后设置decorView的parent为ViewRoorImpl

现在我们的View已经添加到Window上了,并且触发了一次requestLayout(),接下来我们来看它:

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //第一步,检查线程
        checkThread();
        //第二步,刷新标记
        mLayoutRequested = true;
        //第三步,执行更新
        scheduleTraversals();
    }
}

//第一步:检查线程
void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

//第三步:执行更新
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        //刷新标记
        mTraversalScheduled = true;
        //设置同步屏障,同步屏障意味着后面有个异步消息需要优先处理
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //添加异步消息,mChoreographer其实就是个Handler,最终会执行这个mTraversalRunnable
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

//mTraversalRunnable
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //直接执行这个方法
        doTraversal();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        //刷新标记
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        //这里才是重点,开始搞事情
        performTraversals();
        ...
    }
}

//这个方法才是重点,执行了measure/layout/draw,长达900行,这里我们暂提取关键主线代码
private void performTraversals() {
    ...
    //协商测量
    measureHierarchy
    ...
    //测量
    performMeaure();
    ...
    //布局
    performLayout();
    ...
    //绘制
    performDraw();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

小结 我们在经过ViewRootImpl.requestLayout()后,View的内容就绘制在屏幕上了,接着在让decor可见,于是就在屏幕上看到了setContentView()设置的布局,对ViewRootImpl不熟悉的,可以看下 Handler源码分析之二 异步消息的处理

# 总结

  • 1 Activity.setContView(layoutId)初始化了DecorView,这是个FrameLayout;初始化了mContentParent,这是个ViewGroup
  • 2 mContentParent初始化的时候,会根据主题加载系统布局,里面会有个id为content的FrameLayout
  • 3 ActivityThread.handleResumeActivity()会获取WindowManager,然后将DecorView添加到Window
  • 4 添加DecorView的步骤,最终是通过WindowManagerGlobal实现的,会创建ViewRootImpl,然后调用setView()函数
  • 5 在setView函数会指派view的parent为ViewRootImpl自己,会触发requestLayout()
  • 6 requestLayout()是通过post一个异步消息来实现的,最终调用自己的performTraversals()来实现View的测量布局绘制
Last Updated: 1/30/2022, 3:38:46 PM