Android setContentView源码解析

Android开发的同学们对setContentView肯定都不陌生,但凡写到Activity,都离不开这个函数,今天我们就来看看它内部的实现吧!

备注:本文基于Android 8.1.0版本。

1、Activity 与 AppCompatActivity的区别

当我们在老版本Android SDK开发的时候新建的Project的默认继承的是Activity,而在5.0之后默认继承的就是AppCompatActivity。二者的区别从AppCompatActivity的注释中可窥一斑。

/**
 * Base class for activities that use the
 * <a href="{@docRoot}tools/extras/support-library.html">support library</a> action bar features.
 *
 * <p>You can add an {@link android.support.v7.app.ActionBar} to your activity when running on API level 7 or higher
 * by extending this class for your activity and setting the activity theme to
 * {@link android.support.v7.appcompat.R.style#Theme_AppCompat Theme.AppCompat} or a similar theme.
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 *
 * <p>For information about how to use the action bar, including how to add action items, navigation
 * modes and more, read the <a href="{@docRoot}guide/topics/ui/actionbar.html">Action
 * Bar</a> API guide.</p>
 * </div>
 */

翻译过来就是AppCompatActivity是所有使用了Support包中 ActionBar特性的Activity的父类。

关系可以这么形容:AppCompatActivity————>FragmentActivity————>Activity。

2、setContentView

AppCompatActivity中的setContentView也非常简洁,可以看出来需要去代理类中继续查看代码。

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
    // 真正到了这里
    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        if (Build.VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

而代理类实现的setContentView是在AppCompatDelegateImplV9中实现的:

    @Override
    public void setContentView(View v) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        contentParent.addView(v);
        mOriginalWindowCallback.onContentChanged();
    }

3、createSubDecor

setContentView的第一步就是确保SubDecor被install,下面源码中有注释

    // 此处可以看出SubDecor是一个ViewGroup
    private ViewGroup createSubDecor() {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            // 这个错大家可能遇到过,当使用了AppCompatActivity但是没有设置一个Theme.AppCompat的主题,则会报这个Exception。
            throw new IllegalStateException(
                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        }
        // 接下来就到了设置一些Window属性的地方,下面会再说
        if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
            requestWindowFeature(Window.FEATURE_NO_TITLE);// 设置无title
        } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
        }
        if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
            requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
        }
        if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
            requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
        }
        mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
        a.recycle();
        // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();// 就是创建DecorView
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
        // 根据标记来决定inflate哪个layout
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                // If we're floating, inflate the dialog title decor
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);
                // Floating windows can never have an action bar, reset the flags
                mHasActionBar = mOverlayActionBar = false;
                ......
            } else if (mHasActionBar) {
                ......
            }
        } else {
        }
        if (subDecor == null) {
            throw new IllegalArgumentException(
                    "AppCompat does not support the current theme features: { "
                            + "windowActionBar: " + mHasActionBar
                            + ", windowActionBarOverlay: "+ mOverlayActionBar
                            + ", android:windowIsFloating: " + mIsFloating
                            + ", windowActionModeOverlay: " + mOverlayActionMode
                            + ", windowNoTitle: " + mWindowNoTitle
                            + " }");
        }
        if (mDecorContentParent == null) {
            mTitleView = (TextView) subDecor.findViewById(R.id.title);
        }
        // Make the decor optionally fit system windows, like the window's decor
        ViewUtils.makeOptionalFitsSystemWindows(subDecor);
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
        // 获取PhoneWindow中的content布局对象
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            // There might be Views already added to the Window's content view so we need to
            // migrate them to our content view
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            // Change our content FrameLayout to use the android.R.id.content id.
            // Useful for fragments.
            windowContentView.setId(View.NO_ID);
            // 将contentView的id设置为android.R.id.content
            contentView.setId(android.R.id.content);
            // The decorContent may have a foreground drawable set (windowContentOverlay).
            // Remove this as we handle it ourselves
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);
        contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
            @Override
            public void onAttachedFromWindow() {}
            @Override
            public void onDetachedFromWindow() {
                dismissPopups();
            }
        });
        return subDecor;
    }

3.1 requestWindowFeature

    @Override
    public boolean requestWindowFeature(int featureId) {
        ......
        switch (featureId) {
            case FEATURE_SUPPORT_ACTION_BAR:
                throwFeatureRequestIfSubDecorInstalled();
                mHasActionBar = true;// 仅仅是对变量赋值
                return true;
        ......
        }
        return mWindow.requestFeature(featureId);
    }
    // 这个又解释了一个原因,我们如果在setContentView之后再次去设置requestWindowFeature,会抛出Exception。
    private void throwFeatureRequestIfSubDecorInstalled() {
        if (mSubDecorInstalled) {
            throw new AndroidRuntimeException(
                    "Window feature must be requested before adding content");
        }
    }

3.2 mWindow.getDecorView()

各位小伙伴应该都知道Android里的Window这个类的实现子类其实是PhoneWindow,所以我们直接取PhoneWindow中去查getDecorView这个函数。最终会走到这里,注意下面两个标注了重点的地方

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            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 = generateLayout(mDecor);// 重点
            ......
        }
    }    
    // generateDecor最后只是new了一个DecorView
    protected DecorView generateDecor(int featureId) {
        ......
        return new DecorView(context, featureId, this, getAttributes());
    }
    // 看一下DecorView的定义可以看出它是一个FrameLayout
    /** @hide */
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    }

generateLayout函数过多,此处不贴出代码,值只分析下过程:

  1. 设置一些Window的属性;
  2. 根据Window属性选择一个layoutResource,这些layoutResource有一个共性是都有一个@android:id/content的布局,因为在AppCompatDelegateImplV9的createSubDecor函数里会用到这个content;
  3. 选出layoutResource之后会进入一句关键的代码:mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);layoutResource就被inflate出来并且添加到DecorView中了。备注,添加View的时候使用的LayoutParams是MATCH_PARENT;
    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        mStackId = getStackId();
        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }
        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);// inflate出View
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

3.3 再回到createSubDecor

此时就开始创建真正的subDecor了,也有四个可选的layout,根据之前设置的属性来选择,然后去inflate出来。

        // SubDecor中也一定有这个id
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
        // 这里的content就是是PhoneWindow中的content,上面提到过
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            // There might be Views already added to the Window's content view so we need to
            // migrate them to our content view
            // 合并PhoneWindow中的view到SubDecor中的content中
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            // Change our content FrameLayout to use the android.R.id.content id.
            // Useful for fragments.
            // id在这里发生了变化
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
            // The decorContent may have a foreground drawable set (windowContentOverlay).
            // Remove this as we handle it ourselves
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }

3.4 mWindow.setContentView

开始设置PhoneWindow的contentView,再把代码切到PhoneWindow中

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        // mContentParent是不是看起来有点熟悉呢?generateLayout函数的返回值就是它
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);// 有过渡动画的情况下用到了Scene
        } else {
            // 备注,mContentParent之前是@android:id/content,现在是View.NO_ID;
            // 现在的@android:id/content是SubDecor中的action_bar_activity_content
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

备注:到了这里,SubDecor 已经被添加到了PhoneWindow中,并且@android:id/content是SubDecor中的action_bar_activity_content。接下来别的操作是关于细节的设置。

4. 再回到setContentView

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

此时我们可以看出setContentView中最复杂的代码就是ensureSubDecor,接下来的代码就只是使用SubDecor中的content,将我们传入的layout inflate出来然后加进去。

5、总结

setContentView的过程就是通过PhoneWindow创建DecorView,然后创建SubDecor,最终将传递进来的布局add进来。

这样大家也更容易明白为什么通过一些性能分析工具查看布局层次及数量的时候总是比我们自己写的Layout多,也更容易明白对Activity设置View的函数被命名为setContentView。

原文发布于微信公众号 - 双十二技术哥(gh_b0e7544783e2)

原文发表时间:2018-06-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android干货

浅谈RecyclerView(完美替代ListView,GridView)

5226
来自专栏developerHaoz 的安卓之旅

手把手教你从零开始做一个好看的 APP - Day four

本文为 手把手教你从零开始做一个好看的 APP - Day four ,如果想看该系列的其他文章,请点击以下连接

912
来自专栏向治洪

android wheelview实现三级城市选择

很早之前看淘宝就有了ios那种的城市选择控件,当时也看到网友有分享,不过那个写的很烂,后来(大概是去年吧),我们公司有这么一个项目,当时用的还是网上比较流行的那...

4296
来自专栏向治洪

android自定义状态栏颜色

我们知道IOS上的应用,状态栏的颜色总能与应用标题栏颜色保持一致,用户体验很不错,那安卓是否可以呢?若是在安卓4.4之前,答案是否定的,但在4.4之后,谷歌允...

3216
来自专栏Samego开发资源

图片自动轮播图

3596
来自专栏Android先生

从源码的角度浅谈Activity、Window、View之间的关系

讲个很简单的例子,这一天天气甚好,小明外出写生,小明背了一包东西,画板啊,纸啊,笔啊什么的,然后小明找了一处风景甚好的地方,从包里拿出画板,纸,笔然后开始画画,...

1182
来自专栏Winter漫聊技术

Android : 四行代码,优雅返回

为了防止用户误触返回键,还在使用 “再按一次退出” 吗? 追求简约与极速的时代,这种交互显然已经Out了嘛~

1442
来自专栏Sorrower的专栏

界面无小事(二): 让RecyclerView展示更多不同视图

1342
来自专栏Android开发指南

3.CursorAdapter

39415
来自专栏我就是马云飞

我奶奶都能懂的UI绘制流程(上)

前言 从今天开始,慢慢整理Android高级UI的知识,涉及到各种酷炫狂拽吊炸天的特效。 之前写过一篇Window一本满足算是这个专题的预备知识,本文就基于这篇...

2176

扫码关注云+社区

领取腾讯云代金券