setContentView背后的故事

作者: AxeChen | 来源:发表于2017-11-13 23:54 被阅读432次

    Android程序员都知道Activity调用setContentView的方法是将xml布局文件加载到Activity中。那么:
    调用setContentVIew后到底是怎样将xml布局文件加载到Activity中去的?
    继承AppCompatActivity和继承Activity的Activity布局结构有什么不同?
    接下来从源码的角度分析setContentView背后的逻辑。

    因为分析源码,所以下面的内容可能会引起深度不适。

    吐血

    阅读源码的建议:阅读源码一定要带着目的去阅读。比如说:调用setContentVIew后到底是怎样将xml布局文件加载到Activity中去的?然后针对这个问题去寻找答案。如果没有目的性的看源码会非常艰难。还有就是看源码真的很累。在写这篇文章时,源码真的是看了一遍又一遍。所以贵在坚持吧。

    1、从Activity的setContentVIew开始

    API22之后,出现了AppCompatActivity。所有的Activity默认继承它,在AppCompatActivity中使用代理模式做了许多兼容,和一些新的特性的添加。但AppCompatActivity的基类依旧是Activity。所谓“万变不离其宗”,先看下Activity的setContentVIew,然后再去看AppCompatActivity到底做了怎样的兼容。

    1.1、setContentView的作用

    新建一个MainActivity 继承Activity

      public class MainActivity extends Activity {
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
          }
      }
    

    跳转到Activiy中去看setContentView:

        /**
         * Set the activity content from a layout resource.  The resource will be
         * inflated, adding all top-level views to the activity.
         *
         * @param layoutResID Resource ID to be inflated.
         *
         * @see #setContentView(android.view.View)
         * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
         */
        public void setContentView(@LayoutRes int layoutResID) {
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
    

    注意看这段注释:

        * Set the activity content from a layout resource.  The resource will be
        * inflated, adding all top-level views to the activity.
    

    这段注释的含义:

    调用这个方法会将资源文件中的xml布局文件,把所有的顶级View加载到Activity中去。

    这个和我们平时的理解一样:将资源文件中的布局文件加载到Activity中去。
    请注意,这里有个顶级View的概念,往下看我们会讲到。

    调用setContentView的作用就是将资源文件中的布局文件加载到Activity中去。(直接根据注释下结论确实有点生硬,不过往下看代码,具体去了解调用setContentView是如何将资源文件加载到Activity中去的。)


    1.2、最顶层Window——PhoneWindow

    继续看代码。ActivitysetContentView()中又调用了getWindow()setContentView()方法。

     getWindow().setContentView(view);
    

    Activity中,跳转到getWindow()看这个方法:

        /**
         * Retrieve the current {@link android.view.Window} for the activity.
         * This can be used to directly access parts of the Window API that
         * are not available through Activity/Screen.
         *
         * @return Window The current window, or null if the activity is not
         *         visual.
         */
        public Window getWindow() {
            return mWindow;
        }
    

    可以看到getWindow()这个方法返回了一个Window
    跳转到Window类,看下Window到底是什么:

      /**
       * Abstract base class for a top-level window look and behavior policy.  An
       * instance of this class should be used as the top-level view added to the
       * window manager. It provides standard UI policies such as a background, title
       * area, default key processing, etc.
       *
       * <p>The only existing implementation of this abstract class is
       * android.view.PhoneWindow, which you should instantiate when needing a
       * Window.
       */
      public abstract class Window { ... }
    

    仔细看如下注释:

     * Abstract base class for a top-level window look and behavior policy.  An
     * instance of this class should be used as the top-level view added to the
     * window manager. It provides standard UI policies such as a background, title
     * area, default key processing, etc.
    

    这段注释的含义:

    用于顶级窗口外观和行为策略的抽象基类。这个类的实例应该被作为顶层视图添加到窗口管理器。它提供了标准的UI策略,例如背景、标题 区域、默认密钥处理等。 他只有一个实现类:android.view.PhoneWindow。

    Window是最顶层Window的抽象基类。它提供了Window外观,行为等的策略。它只有一个实现类PhoneWindow

    注意:上面提到了最顶层View,这里提到了最顶层Window,两者区别是非常大的,后面会讲到两者的关系。

    既然Window只有一个实现类PhoneWindow,那么Activity中的:

      getWindow().setContentView(layoutResID);
    

    实际上调用的就是PhoneWindowsetContentView方法!!


    1.3、PhoneWindow的setContentView

    跳转到PhoneWindowsetContentView方法:

      @Override
      public void setContentView(int layoutResID) {
          // 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.
          if (mContentParent == null) {
              installDecor();
          } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
              mContentParent.removeAllViews();
          }
          if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
              final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                      getContext());
              transitionTo(newScene);
          } else {
              mLayoutInflater.inflate(layoutResID, mContentParent);
          }
          mContentParent.requestApplyInsets();
          final Callback cb = getCallback();
          if (cb != null && !isDestroyed()) {
              cb.onContentChanged();
          }
          mContentParentExplicitlySet = true;
      }
    

    首先大概看下这个方法中的代码,前面是mContentParent初始化的逻辑。
    后面的代码是将Id为layoutResID资源文件的布局加载到mContentParent中去。因为里面有一句非常重要的代码:mLayoutInflater.inflate(layoutResID, mContentParent);

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
    

    只要是Android程序员一定对mLayoutInflater.inflate不陌生。
    这里还有一个是否有过渡动画的判断if (hasFeature(FEATURE_CONTENT_TRANSITIONS))
    如果是true,最后会调用transitionTo(newScene)。这个方法最后也是将Id为layoutResID资源文件的布局加载到mContentParent中去。从transitionTo开始一直跳转进去,最后在Scene类的enter方法中找到答案:

    public void enter() {
            // Apply layout change, if any
            if (mLayoutId > 0 || mLayout != null) {
                // empty out parent container before adding to it
                getSceneRoot().removeAllViews();
                if (mLayoutId > 0) {
                    // 重要的代码
                    LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
                } else {
                    mSceneRoot.addView(mLayout);
                }
            }
          // ... 这里省略部分代码。
        }
    

    了解mLayoutId, mSceneRoot的初始化就能知道答案了,这里就不多做解释。

    PhoneWindow的setContentView方法会将xml资源文件加载到mContentParent中。

    那么问题来了,mContentParent是什么?它又是如何初始化的?


    1.4、PhoneWindow的mContentParent和DecorView

    首先看mContentParent是什么。在PhoneWindow中能找到mContentParent的解释:

        // This is the view in which the window contents are placed. It is either
        // mDecor itself, or a child of mDecor where the contents go.
        ViewGroup mContentParent;
    

    这段注释的意思是:

    这是一个放置Window内容的View。他不是decorView自己,就是decorView的子View。(这里先不管mDecor是什么,后面会提到。

    继续看代码。
    PhoneWindow中的setContentView中看下这个判断:

       if (mContentParent == null) {
            installDecor();
       } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           mContentParent.removeAllViews();
       }
    

    既然这里是mContentParent的非空判断,那么installDecor()方法一定是初始化mContentParent的方法。

    跳转到installDecor()中看是如何初始化mContentParent的:


    1.4.1、PhoneWindow初始化DecorView

    installDecor()在出事化mContentParent前先初始化了DecorView。所以先看下DecorView的初始化以及DecorViewWindow的关系。

     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);
    
            ///...  省略若干代码
            }
        }
    

    首先是mDecor的初始化。generateDecor是初始化mDecor的方法。

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

    PhoneWindow的中,可以看到mDecor的定义:

        // This is the top-level view of the window, containing the window decor.
        private DecorView mDecor;
    

    这段注释的含义是:

    这是Window的顶层View,包含了Window的装饰。

    跳转到DecorViewDecorView的具体代码:

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
      //... 省略很多很多代码
    }
    

    根据以上可以知道DecorView的作用:

    DecorView是一个FrameLayout,是最顶层的View。包含Window(顶级窗口)的装饰(比如大小,是否透明等等属性)会体现在这个View上。这里可以大概知道顶级窗口Window(PhoneWindow)和顶级View(DecorView)的关系:顶级窗口Window(PhoneWindow)包含的状态属性,会在顶级View(DecorView)体现出来,比如窗口大小,背景等属性。(当然下面的代码也会体现出这点)

    回到PhoneWindow,在PhoneWindow中看下DecorView的初始化方法generateDecor():

       protected DecorView generateDecor(int featureId) {
          // System process doesn't have application context and in that case we need to directly use
          // the context we have. Otherwise we want the application context, so we don't cling to the
          // activity.
          Context context;
          if (mUseDecorContext) {
              Context applicationContext = getContext().getApplicationContext();
              if (applicationContext == null) {
                  context = getContext();
              } else {
                  context = new DecorContext(applicationContext, getContext().getResources());
                  // 设置主题
                  if (mTheme != -1) {
                      context.setTheme(mTheme);
                  }
              }
          } else {
              context = getContext();
          }
          // new 一个DecorVIew
          return new DecorView(context, featureId, this, getAttributes());
      }
    

    最后有这样的代码:

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

    那么generateDecor()就是初始化DecorView
    里面的第三个参数,就是PhoneWindow。初始化DecorView时会将PhoneWindow传进去。
    看下这个完整的if判断:

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

    如果DecorView不为空,会调用setWindow()。传入的this也是PhoneWindow。同时跳转到DecorVIew中可以看到类似如下代码:

    public void setWindowBackground(Drawable drawable) {
            if (getBackground() != drawable) {
                setBackgroundDrawable(drawable);
                if (drawable != null) {
                    mResizingBackgroundDrawable = enforceNonTranslucentBackground(drawable,
                            mWindow.isTranslucent() || mWindow.isShowingWallpaper());
                } else {
                    mResizingBackgroundDrawable = getResizingBackgroundDrawable(
                            getContext(), 0, mWindow.mBackgroundFallbackResource,
                            mWindow.isTranslucent() || mWindow.isShowingWallpaper());
                }
                if (mResizingBackgroundDrawable != null) {
                    mResizingBackgroundDrawable.getPadding(mBackgroundPadding);
                } else {
                    mBackgroundPadding.setEmpty();
                }
                drawableChanged();
            }
        }
    

    或者是这样的代码:

        final WindowManager.LayoutParams attrs = mWindow.getAttributes();
        if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
            if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
            }
        }
    

    以上代码是从Window里面获取属性来初始化DecorView的属性,或者根据Window的属性来设置DecorView的属性。这样的代码还有很多,这里不一一贴出,那么上面提到的 window的装饰(比如大小,是否透明等等属性)会体现在DecorView上就解释得通了。(这里也说明了Window和DecorVIew的关系


    1.4.2、PhoneWindow初始化mContentParent

    回到PhoneWindow类。初始化DecorView之后,继续看代码了解mContentParent的初始化:

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

    可以看到,这里generateLayout()方法初始化了mContentParent
    generateLayout()这个方法太长了。截取的几段关键的代码:

     protected ViewGroup generateLayout(DecorView decor) {
            // Apply data from current theme.
    
            // 拿window设置的Style
            TypedArray a = getWindowStyle();
    
            // 是不是浮动的Window ,例如Dialog等
            mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
            int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                    & (~getForcedWindowFlags());
            if (mIsFloating) {
                setLayout(WRAP_CONTENT, WRAP_CONTENT);
                setFlags(0, flagsToUpdate);
            } else {
                setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
            }
    // ... ... 省略若干代码。
    }
    

    首先第一句代码就是:

        TypedArray a = getWindowStyle();
    

    TypedArray,如果写过自定义控件,对于自定义属性的获取绝对不会陌生。
    看下getWindowStyle()的具体实现:

        /**
         * Return the {@link android.R.styleable#Window} attributes from this
         * window's theme.
         */
        public final TypedArray getWindowStyle() {
            synchronized (this) {
                if (mWindowStyle == null) {
                    mWindowStyle = mContext.obtainStyledAttributes(
                            com.android.internal.R.styleable.Window);
                }
                return mWindowStyle;
            }
        }
    

    getWindowStyle()这个方法就是获取window的属性然后返回。
    如果写过自定义控件,在写自定义控件时,获取到自定义属性之后,一般是给自定义View的成员变量赋初始值。来确定自定义View属性的值。这是编写自定义View的一个思路。

    当然!PhoneWindow也是这么干的。继续看下面的代码。
    这里贴出一些代表性的,稍微熟悉点的代码,看看获取到window的style之后都干了些什么。

      // 获取window的属性
       TypedArray a = getWindowStyle();
    
      // 是不是浮动的Window ,例如Dialog等
      mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
            
      // 是不是设置了noTitle的style
      if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {}
       
      // 是不是透明的Window
      mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
    

    以上的代码从TypedArray获取一些值来判断:

    • window是不是可以浮动的window。比如dialog等;
    • 判断是不是设置了Title;
    • 判断是不是透明的window;
    • ... ...

    然后将这些值复制给PhoneWindow的成员变量。
    这个完全和自定View时获取自定义属性和赋初始值的逻辑一模一样!

    获取完Window的属性之后干了什么呢?

    继续往下看代码:

        int layoutResource;
        // 拿到设置属性,然后去加载不同的XML。
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        } else if (){
            // 省略若干代码... ...
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }
       // 将加载的布局文件加载到DecorView中去 
           mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    

    上面的代码首先定义一个layoutResource,然后根据features的值加载不同的布局文件(features也是根据Window设置的属性获取到的值)。然后将布局文件id赋值给layoutResource。接下来的代码回将layoutResource加载到DecorView中去。

    请注意:这里虽然讲的是初始化mContentView,但是这个布局确实是加载到DecorView中去。

    看下这段代码:

      // 将加载的布局文件加载到DecorView中去 
      mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    

    跳转到DecorView看onResourcesLoaded的具体实现:

      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);
          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();
      }
    

    又是mLayoutInflater!又看到了熟悉的面孔,一看到LayoutInflater应该就能想到肯定是和加载布局有关。看下onResourcesLoaded()的思路:
    首先将根据layoutResource加载成一个View:

    final View root = inflater.inflate(layoutResource, null);
    

    然后调用addView()将这个View添加到DecorView中去!

    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    

    这段代码面还有DecorCaptionView的判断。
    DecorCaptionView是什么呢?找到初始化DecorCaptionView的方法.

    // Free floating overlapping windows require a caption.
        private DecorCaptionView createDecorCaptionView(LayoutInflater inflater){
      // ... 省略若干代码
    }
    

    这段注释的意思是:

    自由浮动的重叠窗口需要一个标题。

    自由浮动的窗口?这种情况我在开发中用的比较少。可能是Dialog?所以我暂时不考虑这种情况。只考虑普通的Activity的加载。
    那么加载了什么样的布局到了DecorView中去呢 ?
    回到PhoneWindow``类的generateLayout()看下最简单的这个布局R.layout.screen_simple```

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:theme="?attr/actionBarTheme" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    这个布局是LinearLayoutLinearLayout中有个ViewStubFrameLayout
    android:id="@android:id/content"这个id好像似曾相识。相信有人用过findViewById(android.R.id.content)来获取Activity内容的根布局。

    (如果你手中有本《安卓艺术开发探索》请打开第176页,这里面也有对android.R.id.content的简单的说明)。ps:这个是我突然想起来的。

    如果没用到过这个id。请记住它,下面马上就用到了。

    继续看generateLayout的代码:

    // 找到刚刚的Content
            ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
            if (contentParent == null) {
                throw new RuntimeException("Window couldn't find content container view");
            }
            // ... ... 省略若干代码
            // 最后返回这个contentParent
            return contentParent;
    

    看下这个Id:ID_ANDROID_CONTENT

        /**
         * The ID that the main layout in the XML layout file should have.
         */
        public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
    

    没错!这个Id就是前面讲到的FrameLayout的id。然后通过findById的方法获取到一个ViewGroup。这个ViewGroup就是mContentParent。最后返回这个ViewGroup。


    1.4.3 installDecor()方法的总结

    installDecor()里面包含了大量的逻辑。
    开始初始化了DecorView
    然后初始化mContentParent
    在初始化mContentParent时,又看到:
    获取window的属性并赋值给PhoneWindow的逻辑。
    看到了根据window的属性加载了不同的布局,并加载到给DecorView的逻辑。

    最后看到了通过findViewById的方法获取到了mContentParent并且返回。


    1.4.4 回到PhoneWindow的setContentView

    这个逻辑在前面也写过了。再回顾下PhoneWindow的setContentView的逻辑。

     @Override
        public void setContentView(int layoutResID) {
            // 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.
            if (mContentParent == null) {
                installDecor();
                //不为空诗先RemoviewAllViews
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                mContentParent.removeAllViews();
            }
    
            // 是否存在动画
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                        getContext());
                transitionTo(newScene);
            } else {
                // 吧资源文件添加到 mContentParent 中去
                mLayoutInflater.inflate(layoutResID, mContentParent);
            }
            mContentParent.requestApplyInsets();
            final Callback cb = getCallback();
            if (cb != null && !isDestroyed()) {
                cb.onContentChanged();
            }
            mContentParentExplicitlySet = true;
        }
    

    关键代码:

      // 吧资源文件添加到 mContentParent 中去
      mLayoutInflater.inflate(layoutResID, mContentParent);
    

    前面讲了初始化mContentParent的逻辑,这里将布局文件加载到了mContentParent去。


    1.4.5 流程回顾

    第一步:初始化DecorView

     generateDecor(-1);
    

    第二步:初始化mContentParent

    generateLayout(mDecor);
    

    在初始化mContentParent的同时,看到了初始化PhoneWindow的属性,加载了一个LinearLayoutDecoview中。
    第三步:将布局加载到mContentParent中

     public void setContentView(int layoutResID) {
           mLayoutInflater.inflate(layoutResID, mContentParent);
     }
    

    画图来表示一下继承Activity时的布局结构:

    继承Activity时的布局结构

    回顾下前面的东西

    如果以上的内容已经看吐了,确实我写这个玩意也写吐了。但是还得继续写下去,因为还有继承AppCompatActivity的setContentView。
    如果你能跟着上面博客看完,并且能看懂的,我表示非常感谢。先不要急着看下面的内容。先给自己一杯热茶或者咖啡,走动看看窗外。回想下setContentView()之后的流程,回想一下DecorView,mContentPerant是如何初始化的。能弄懂以上才能更容易看懂继承AppCompatActivity的setContentView的流程。


    2、AppCompatActivity的setContentVIew

    接下来看下继承AppCompatActivity的setContentView。

      public class MainActivity extends AppCompatActivity {
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
          }
      }
    

    进入AppCompatActivity的setContentView:

     @Override
     public void setContentView(@LayoutRes int layoutResID) {
         getDelegate().setContentView(layoutResID);
     }
    

    可以看到从这里开始,就和继承ActivitysetContentView逻辑不同了。那么getDelegate()是什么呢?
    从delegate我们可以参想这个应该是个代理模式。代理模式这边暂时不做研究。


    2.1、AppCompat的AppCompatDelegate

    跳转到getDelegate方法:

        @NonNull
        public AppCompatDelegate getDelegate() {
            if (mDelegate == null) {
                mDelegate = AppCompatDelegate.create(this, this);
            }
            return mDelegate;
        }
    

    可以看到这里就是初始化了AppCompatDelegate。
    跳转到AppCompatDelegate它有一行注释:

    This class represents a delegate which you can use to extend AppCompat's support to any
    

    这个注释的意思是:这个类表示一个代理,您可以使用它来扩展AppCompat的支持。那么AppCompatDelegate用来做什么呢?先继续往下看create方法的代码:

      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);
            }
        }
    

    可以看到,这里根据版本判断返回了不同的AppCompatDelegate的实现。这些AppCompatDelegateImpl都继承自AppCompatDelegateImplV9。而且最终调用的是AppCompatDelegateImplV9setContentView
    直接跳转到AppCompatDelegateImplV9查看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();
        }
    

    AppCompatDelegate到底用来干什么的呢?
    AppCompatDelegate是一个可以放在任意Activity中,并且回调相应生命周期的类,在不使用AppCompatActivity的情况下,也可以得到一致的主题与颜色,用于兼容不同版本的控件。
    具体看看这两篇博客:
    http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0422/2774.html
    http://www.jianshu.com/p/66fa79ec0109


    2.2 初始化SubDecorView

    先看第一个方法:ensureSubDecor()

      private void ensureSubDecor() {
            if (!mSubDecorInstalled) {
                mSubDecor = createSubDecor();
    
                // If a title was set before we installed the decor, propagate it now
                CharSequence title = getTitle();
                if (!TextUtils.isEmpty(title)) {
                    onTitleChanged(title);
                }
                applyFixedSizeWindow();
                onSubDecorInstalled(mSubDecor);
                mSubDecorInstalled = true;
    
                // Invalidate if the panel menu hasn't been created before this.
                // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
                // being called in the middle of onCreate or similar.
                // A pending invalidation will typically be resolved before the posted message
                // would run normally in order to satisfy instance state restoration.
                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
                if (!isDestroyed() && (st == null || st.menu == null)) {
                    invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
                }
            }
          }
    

    从以上代码,可以看到这里初始化了mSubDecor
    看下定义mSubDecor的地方:

        // true if we have installed a window sub-decor layout.
        private boolean mSubDecorInstalled;
        private ViewGroup mSubDecor;
    

    这个注释的意思是:

      如果mSubDecorInstalled为true,则我们出事化了一个窗口的子装饰布局。
    

    窗口的子装饰布局? 先不管这是什么,先记住他。
    createSubDecor()方法初始化了mSubDecor。跳转进去看下:

      private ViewGroup createSubDecor() {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            //... ... 省略很多代码
            if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
            }
            mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
            //... ... 省略很多代码
            // Now let's make sure that the Window has installed its decor by retrieving it
            mWindow.getDecorView();
    }
    

    好嘛,又见到了TypedArray,但是这次是取的AppCompatTheme。这回是取AppCompatTheme里面的style。这次不是赋值给PhoneWindow了,这次是赋值给AppCompatDelegateImplBase里面的成员变量。实际上也是AppCompatDelegateImplV9的成员变量,因为AppCompatDelegateImplV9继承自AppCompatDelegateImplBase
    不知道有开发者有没有注意到这个问题,如果Activity继承自AppCompatActivity,如果没有给这个Activity设置一个AppCompat的Theme。就会崩溃。原因就在这里了。

       if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
             a.recycle();
            throw new IllegalStateException(
                        "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        }
    
    

    如果继承了AppCompatActivity没有设置Theme。抛出的异常一定是:

     "You need to use a Theme.AppCompat theme (or descendant) with this activity.
    

    2.2.1 初始化SubDecorView前,先初始化DecorView和mContentParent

    初始化DecorView和mContentParent在前面已经了解了。

    接下来看下非常重要的代码:

        // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();
    

    mWindow从AppCompatDelegateImplBase中的定义,可以知道它就是PhoneWindow。
    那么跳转到PhoneWindow的getDecorView()方法。

        @Override
        public final View getDecorView() {
            if (mDecor == null || mForceDecorInstall) {
                installDecor();
            }
            return mDecor;
        }  
    

    好嘛,installDecor() 又是它。回顾已经讲过的installDecor()做了什么吧:
    installDecor()方法的作用:

    初始化DecorView,出事化mContentParent并且加载一个LinearLayout。那么getDecorView就是初始化了DecorView。

    继续往下看:

     final LayoutInflater inflater = LayoutInflater.from(mContext);
            ViewGroup subDecor = null;
            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) {
            }        
          }
    

    这个逻辑又和前面初始化mContentParent并且加载一个LinearLayout的逻辑一样。

    <android.support.v7.widget.FitWindowsLinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/action_bar_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:fitsSystemWindows="true">
    
        <android.support.v7.widget.ViewStubCompat
            android:id="@+id/action_mode_bar_stub"
            android:inflatedId="@+id/action_mode_bar"
            android:layout="@layout/abc_action_mode_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
        <include layout="@layout/abc_screen_content_include" />
    
    </android.support.v7.widget.FitWindowsLinearLayout>
    
    <merge xmlns:android="http://schemas.android.com/apk/res/android">
    
        <android.support.v7.widget.ContentFrameLayout
                android:id="@id/action_bar_activity_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:foregroundGravity="fill_horizontal|top"
                android:foreground="?android:attr/windowContentOverlay" />
    
    </merge>
    

    可以看到这个和已经讲过的初始化mContentParent时加载LinearLayout差不多。请记住ContentFrameLayout这个View的id: android:id="@id/action_bar_activity_content" 继续看代码:


    2.2.2 ContentFrameLayout 和mContentParent的相互替换

     final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                    R.id.action_bar_activity_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.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);
    

    这段代码的逻辑:
    首先从subDecor中通过findViewById的方法获取到ContentFrameLayout
    然后通过PhoneWindowfindViewById方法获取到了DecorView中的LinearLayout中的FrameLayout
    然后遍历DecorView中的FrameLayout将它的子View添加到ContentFrameLayout
    这个时候 DecorView中的FrameLayout就没有子VIew了。

    // 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);
    
    

    然后把DecorView中的FrameLayout的Id设为NO_ID
    ContentFrameLayout设为android.R.id.content
    这里就是 ContentFrameLayout替换了DecorView中的FrameLayout的过程。

       mWindow.setContentView(subDecor);
    

    然后调用mWindow.setContentView(subDecor);把SubDecorVIew加载到PhoneWIndow的mContentParent中去。就这样继承AppCompatActivity的Activity调用setContentView就这样完成了。

    前面提到过SubDecorVIew是窗口的子装饰布局,这里确实将SubDecorVIew加载到了DecorView中。


    2.3 画图来表示继承AppCompatActivity的setContentView的流程。

    继承AppCompatActivity的setContentView()最后调用的是createSubDecor()。Acitvity加载布局的逻辑都在这个方法中:
    第一步:初始化DecorView和mContentParent
    代码为:

            // Now let's make sure that the Window has installed its decor by retrieving it
            mWindow.getDecorView();
    
    初始化DecorView和mContentParent

    第二步:初始化SubDecorView
    代码为:

      final LayoutInflater inflater = LayoutInflater.from(mContext);
            ViewGroup subDecor = null;
    // ... ... 省略若干代码
     subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
    
    初始化SubDecorView

    第三步:SubDecorView和DecorView中的mContentParent替换的过程
    代码为:

     final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                    R.id.action_bar_activity_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.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);
                }
            }
    
    
    SubDecorView和DecorView中的mContentParent替换的过程

    第四步:将SubDecorView加载到DecorView中去
    代码为:

            // Now set the Window's content view with the decor
            mWindow.setContentView(subDecor);
    
    将SubDecorView加载到DecorView中去

    第五步:将资源文件中的布局添加到ContentFrameLayout中
    代码为:

     @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();
        }
    

    最后继承AppCompatActivity的布局结构:


    继承AppCompatActivity的布局结构

    看到这么多层的布局不要惊讶,事实就是这样。

    3、总结

    以上就是继承Activity和继承AppCompatActivity的逻辑和布局结构。如果已经看懵逼了就看下画了图的内容吧,那是总结出来的精华。

    有些话写在最后

    如果最后有人看完了这篇博客并且大概了解博客讲的东西,真的非常感谢。看源码对于现在的我来说真的是一个非常痛苦的过程。为了尽量写好这篇文章源码看了一遍又一遍。写完之后确实是非常轻松的,因为确实学到了不少干货。

    相关文章

      网友评论

        本文标题:setContentView背后的故事

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