阅读 951

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。

广告时间

今日头条各Android客户端团队招人火爆进行中,各个级别和应届实习生都需要,业务增长快、日活高、挑战大、待遇给力,各位大佬走过路过千万不要错过!

本科以上学历、非频繁跳槽(如两年两跳),欢迎加我的微信详聊:KOBE8242011

欢迎关注

关注下面的标签,发现更多相似文章
评论