美文网首页
setContentView源码分析

setContentView源码分析

作者: CP9 | 来源:发表于2017-05-12 16:28 被阅读114次

    Activity调用setContentView将布局添加到窗口的流程如图:

    setContentView_flow.png

    在深入了解setContentView之前,先提出以下疑问:

    1. 为什么调用setContentView就能将布局显示出来?
    2. 为什么requestFeature需要在setContentView之前调用?
    3. PhoneWindow和Window之间有什么关系?
    4. DecorView和我们的布局有什么关系?
    5. 为什么继承AppCompactActivity后,主题需要继承AppCompactTheme?

    Window和PhoneWindow

    Activity有三个setContentView重载方法:

        public void setContentView(@LayoutRes int layoutResID) {
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
        public void setContentView(View view) {
            getWindow().setContentView(view);
            initWindowDecorActionBar();
        }
        public void setContentView(View view, ViewGroup.LayoutParams params) {
            getWindow().setContentView(view, params);
            initWindowDecorActionBar();
        }
    

    从Activity的setContentView方法的实现来看第一步会调用Window的setContentView方法,那我们就来看看Window类,从注释中可以得知这个类的实例是被当做顶级View添加到了WindowManager中,由WindowManager管理。而PhoneWindow是抽象类Window的唯一子类,他们之间的关系如下图:

    Window_class.png

    来看看Window中的几个重要的方法:

    加载Window的主题

    通过Window的getWindowStyle方法从style.xml中获取此应用程序窗口主题的属性,这个属性定义在platform_frameworks_base/core/res/res/values/attrs.xml

    synchronized (this) {
        if (mWindowStyle == null) {
            mWindowStyle = mContext.obtainStyledAttributes(
                    com.android.internal.R.styleable.Window);
        }
        return mWindowStyle;
    }
    

    Window#findViewById

    这个方法是我们最常用的方法之一,在Activity中调用findViewById方法,内部会调用Window的findViewById方法,最终调用的是View中的findViewById方法,这里不做深入研究。

    return getDecorView().findViewById(id);
    

    Window#setContentView(int)

    在Window中该方法是抽象方法,查看它的唯一子类PhoneWindow中的实现。由于这个方法有三个重载方法,我们重点关注setContentView(int)方法,另外两个重载方法大同小异。

    PhoneWindow#setContentView(int)

    1. 调用installDecor()方法初始化mDecor和mContentParent,当再次调用setContentView方法时,如果没有添加场景转换动画,mContentParent会移除所有添加的View
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    
    1. 如果添加了场景转换动画,会执行此动画效果;否则调用LayoutInflater的inflate()方法将布局添加到mDecor的mContentParent中
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    }else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    

    这里出现了一个关键成员变量mContentParent,看注释得知这个成员变量是一个用来存放应用程序窗口内容的View,它有可能是mDecor本身,或是mDecor下的子View。而mDecor是应用程序窗口的顶级View。


    DecorView的创建过程

    初始化PhoneWindow

    PhoneWindow的初始化是在Activity的attach方法中调用的

    mWindow = new PhoneWindow(this, window);
    

    创建DecorView —— mDecor

    DecorView是在PhoneWindow中的generateDecor方法中创建的

    ...
    return new DecorView(context, featureId, this, getAttributes());
    

    并在PhoneWindow中的installDecor方法赋值给成员变量mDecor

    if (mDecor == null) {
          mDecor = generateDecor(-1);
          ...
    }
    

    然后会在Activity启动过程中,将DecorView添加到PhoneWindow,可以参考DecorView是如何添加到窗口的?

    创建ViewGroup —— mContentParent

    DecorView是在PhoneWindow中的generateLayout方法中创建的

    1. 获取TypedArray
    TypedArray a = getWindowStyle();
    
    1. 根据TypedArray得到的属性设置是否启用屏幕的一些特性
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
                requestFeature(FEATURE_NO_TITLE);
    }
    ...
    
    1. 根据第二步设置的Features得到不同的layoutResource,并通过DecorView的onResourcesLoaded方法将layoutResource添加到DecorView中
    int features = getLocalFeatures();
    if ...
    else{
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
    }
    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    ...
    
    1. 创建mContentParent
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    1. 为顶层窗口设置背景和标题
    ...
    final Drawable background;
    if (mBackgroundResource != 0) {
        background = getContext().getDrawable(mBackgroundResource);
    } else {
        background = mBackgroundDrawable;
    }
    mDecor.setWindowBackground(background);
    if (mTitle != null) {
        setTitle(mTitle);
    }
    if (mTitleColor == 0) {
        mTitleColor = mTextColor;
    }
    ...
    

    并在PhoneWindow中的installDecor方法赋值给成员变量mContentParent

    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        ...
    }
    

    从上可以得知mContentParent是DecorView下的一个id为content的ViewGroup,一般是FrameLayout

    PhoneWinow#requestFeature

    该方法用来设置主窗口的各种特性,例如是否显示标题栏、是否悬浮等,在Activity中使用requestWindowFeature来设置,内部会自己调用PhoneWinow的requestFeature方法。从mContentParent的创建过程可知requestFeature方法需要在setContentView之前调用的原因。让我们来看看一些实际的运用

    根据上面的分析可以得到在Activity中View的布局结构图如下:

    Activity的View的布局结构图.png

    兼容包AppCompatActivity的setContentView流程

    看过了Activity的setContentView之后,我们来看看经常使用的AppCompatActivity的setContentView有什么不同。

    getDelegate().setContentView(layoutResID);
    

    这个getDelegate方法是用来兼容我们各个版本的:

    AppCompatActivity#getDelegate

    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    

    AppCompatDelegate#create

    if (BuildCompat.isAtLeastN()) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (sdk >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else if (sdk >= 14) {
        return new AppCompatDelegateImplV14(context, window, callback);
    } else if (sdk >= 11) {
        return new AppCompatDelegateImplV11(context, window, callback);
    } else {
        return new AppCompatDelegateImplV9(context, window, callback);
    }
    

    以API25为例,这时候会创建一个AppCompatDelegateImplN代理类,从AppCompatDelegateImplN的父类
    AppCompatDelegateImplV9找到了setContentView方法的具体实现:

    AppCompatDelegateImplV9#setContentView

    1. 确保subDecor是否创建,如果没有则创建
    ensureSubDecor();
    
    1. 将AppCompatActivity中setContentView中传入的布局添加到subDecor中id为content的FrameLayout
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    
    1. 回调onContentChanged方法
    mOriginalWindowCallback.onContentChanged();
    

    AppCompatDelegateImplV9#ensureSubDecor

    1. 如果subDecor没有创建过,则创建
    mSubDecor = createSubDecor();
    
    1. 如果在subDecor创建之前就设置了标题,在这里回调onTitleChanged
    // If a title was set before we installed the decor, propagate it now
    CharSequence title = getTitle();
    if (!TextUtils.isEmpty(title)) {
        onTitleChanged(title);
    }
    
    1. 将标记设置为true
    mSubDecorInstalled = true;
    

    AppCompatActivity中DecorView的创建 —— AppCompatDelegateImplV9#createSubDecor

    加载Window的主题

    创建subDecor的时候使用的是AppCompatTheme,此declare-styleable在AppCompatV7源码的res\values\values.xml文件中定义的,这就是为什么我们的在style.xml中定义的主题需要继承AppCompatTheme的原因

    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    

    创建DecorView

    调用PhoneWindow中的getDecorView方法,内部会调用installDecor方法,从这则回到了Activity中调用setContentView的流程

    mWindow.getDecorView();
    

    PhoneWindow#getDecorView

    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
    

    创建subDecor

    此subDecor并不是DecorView,只是模拟Activity中的mDecor,类似Activity中DecorView的创建,不过这里subDecor的布局是各种兼容布局

    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) {
            // Now inflate the view using the themed context and set it as the content vi
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);
            mDecorContentParent = (DecorContentParent) subDecor
                    .findViewById(R.id.decor_content_parent);
            mDecorContentParent.setWindowCallback(getWindowCallback());
            ...
        }
    } else {
        if (mOverlayActionMode) {
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_screen_simple_overlay_action_mode, null);
        } else {
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        }
        if (Build.VERSION.SDK_INT >= 21) {
            // If we're running on L or above, we can rely on ViewCompat's
            // setOnApplyWindowInsetsListener
            ...
        } else {
            // Else, we need to use our own FitWindowsViewGroup handling
            ...
        }
    }
    

    让系统的mDecor中加载的是兼容的布局

    1. 获取subDecor中的存放内容布局的的兼容FrameLayout,和PhoneWindow中的mDecor中存放内容布局的FrameLayout
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);
    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    
    1. 将PhoneWindow中的mDecor中的内容布局从mDecor中移除,添加到subDecor中,并修改其id
    // 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.setId(android.R.id.content);
    
    1. 将subDecor作为内容布局传给PhoneWindow中
    // Now set the Window's content view with the decor
    mWindow.setContentView(subDecor);
    

    根据上面的分析可以得到在AppCompatActivity中View的布局结构图如下:

    AppCompatActivity的View布局结构.png

    总结

    至此我们已经分析完了setContentView的源码,对于之前提的疑问也有了答案:

    1. 为什么调用setContentView就能将布局显示出来?
      调用setContentView方法内部会调用PhoneWindow的setContentView方法,其内部通过mLayoutInflater.inflate(layoutResID, mContentParent);加载到DecorView的子布局mContentParent中,而DecorView是我们的顶级View,会在Activity启动后加载到当前Activity的应用程序窗口,所以我们调用setContentView就能将我们的布局显示出来。
    2. 为什么requestFeature需要在setContentView之前调用?
      当我们在Activity中调用了setContentView方法,会调用PhoneWindow的generateLayout方法,该方法会根据requestFeature方法设置的属性来选择DecorView中加载的布局,以及根据一些特性,例如是否显示标题,来设置当前窗口的特性。
    3. PhoneWindow和Window之间有什么关系?
      当我们在Activity中调用了setContentView方法,内部会调用Window的setContentView方法,Window是一个抽象类,而PhoneWindow是抽象类Window的唯一子类。Window的实例必须当做顶级View添加到WindowManager中。
    4. DecorView和我们的布局有什么关系?
      DecorView是我们窗口的顶级View,意味着我们使用Hierarchy Viewer查看View的层级关系时,最上层的View都是DecorView。我们的布局是加载在DecorView下的一个id为content的FrameLayout中的。
    5. 为什么继承AppCompactActivity后,主题需要继承AppCompactTheme?
      在AppCompactActivity中调用setContentView,内部会调用AppCompatDelegateImplV9的createSubDecor方法,其中会加载兼容Window的主题AppCompactTheme

    相关文章

      网友评论

          本文标题:setContentView源码分析

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