Android setContentView源码解析

作者: 未来的理想 | 来源:发表于2018-06-09 15:33 被阅读33次

    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

    欢迎关注

    相关文章

      网友评论

        本文标题:Android setContentView源码解析

        本文链接:https://www.haomeiwen.com/subject/afiteftx.html