Android setContentView源码解析

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

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

一、Activity 与 AppCompatActivity的区别

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

/**
 * 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的父类。bash

关系能够这么形容:AppCompatActivity————>FragmentActivity————>Activity。微信

二、setContentView

AppCompatActivity中的setContentView也很是简洁,能够看出来须要去代理类中继续查看代码。markdown

@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中实现的:app

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

三、createSubDecor

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

// 此处能够看出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出来而后加进去。

五、总结

setContentView的过程就是经过PhoneWindow建立DecorView,而后建立SubDecor,最终将传递进来的布局add进来。

这样你们也更容易明白为何经过一些性能分析工具查看布局层次及数量的时候老是比咱们本身写的Layout多,也更容易明白对Activity设置View的函数被命名为setContentView。

广告时间

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

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

欢迎关注
相关文章
相关标签/搜索