当我们给Activity设置布局时,都是直接调用setContentView来完成的,但具体Android是怎么把布局加载到window,又是怎么通过findViewById获取view对象的,我们可能并没有太关心,下面就结合源码来分析下这个过程。

Android setContentView

打开Activity的源码发现,setContentView有三个重载方法,

  1. public void setContentView(int layoutResID);
  2. public void setContentView(View view);
  3. public void setContentView(View view, ViewGroup.LayoutParams params)
    我们就来看下最常用的第一个方法:
public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这个方法调用了,Window类中的setContentView()方法,其他方法也是调用了Window类中的setContentView(),但是Window是一个抽象类,在Activity的attach方法中被初始化,其实是一个PhoneWindow实例,所以这个setContentView方法在PhoneWindow中实现。

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);
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

首先判断mContentParent是否为空,如果为空的话则调用installDecor()方法,其次判断是否设置了FEATURE_CONTENT_TRANSITIONS属性,如果没有的话则移除所有view(从这里我们可以得出setContentView可以调用多次,反正会removeAllViews),然后调用LayoutInflater.inflate(),将我们设置的布局文件添加到mContentParent中。接着获取了一个Callback对象,那这个是在Activity的attach方法中设置的一个回调

mWindow.setCallback(this);

所以可以得出在Activity中一定有一个onContentChanged回调,我们来看下这个回调

public void onContentChanged() {}

额,空空如也。但是我们可以在自己的Activity中重写这个回调,用于在setContentView之后做一些事情,比如findViewById,但貌似实际场景也不需要。。。
好了,现在我们回到上面提到的installDecor()方法,好长,我们捡重要的看吧。

private void installDecor() {
              //初始化decorView
            if (mDecor == null) {
                mDecor = generateDecor();
                mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
                mDecor.setIsRootNamespace(true);
                if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
                }
            }
            //初始化mContentParent
            if (mContentParent == null) {
                mContentParent = generateLayout(mDecor);
                  ......
                  //设置一堆属性值
            }
    }

看下PhoneWindow中的generateDecor()方法

protected DecorView generateDecor() {
          return new DecorView(getContext(), -1);
    }

只是单纯的new了一个DecorView实例。这个DecorView是什么鬼。其实它是PhoneWindow的一个内部类,是整个window界面最顶层的view。包含ActionBar,内容块等。好了,现在我们缕一下Window,PhoneWindow,decorView的关系

1.Window类是一个抽象类,提供了绘制窗口的一组通用API。可以将之理解为一个载体,各种View在这个载体上显示。
2.PhoneWindow是Window的一个子类,是Window的具体实现,包含一个内部类DecorView,PhoneWindow是将decorView进行了一定包装,并提供一些方法用于操作窗口。
3。DecorView继承自FrameLayout,是窗口的根view。

好了,接着看mContentParent的初始化,generateLayout(mDecor).这里传入了上一部初始化好的DecorView. 又是一个长方法,我们还是挑出重要的部分。

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        TypedArray a = getWindowStyle();

        //......
        //根据定义的style设置一些值,比如是否显示ActionBar,

        // Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
        //......
        //根据设定好的features值选择不同的窗口修饰布局文件,
        //得到layoutResource值,系统定义了不同的layout,比如  
        //R.layout.screen_custom_title,R.layout.screen_simple

        //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        //......
        //继续一堆属性设置,返回contentParent
        return contentParent;
    }

根据不同的features值,设定layoutResource,最终添加到decorView中,所以我们通过在xml中设置的theme,还有在代码中设置的requestWindowFeature,都是用来设置features值,这也是为什么requestWindowFeature方法必须在setContentView之前的原因。
这样看来,如果我们设置我们的Theme为NoTitleBar,最终layoutResource的值为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>

来看下去处标题栏后的视图树

setContentView

所以installDecor主要是初始化了PhoneWindow中的DecorView.和contentParent,之后在setContentView()中通过mLayoutInflater.inflate(layoutResID, mContentParent);将layoutResId,add到初始化好的contentParent中。
大家是否好奇状态栏怎么被加载进DecorView的,我们来看下DecorView中的updateColorViewInt方法

private View updateColorViewInt(View view, int sysUiVis, int systemUiHideFlag,
            int translucentFlag, int color, int height, int verticalGravity,
            String transitionName, int id, boolean hiddenByWindowFlag) {
          ......
        if (view == null) {
            if (show) {
                view = new View(mContext);
                view.setBackgroundColor(color);
                view.setTransitionName(transitionName);
                view.setId(id);
                addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height,
                        Gravity.START | verticalGravity));
            }
        } else {
           ......
               }
        return view;
    }

可以看到直接new了一个view,这个view就是状态栏,然后将状态栏添加到了DecorView,其实这个状态栏只是一个单纯的占位view。被updateColorViews方法调用,比如当我们调用setStatusBarColor时就是调用了updateColorViews这个方法。这里先不做过多介绍。

findViewById

那么将layout添加进decorView中后,我们是怎么通过findViewById找到View的呢?
看下Activity的findViewById方法

/**
 * Finds a view that was identified by the id attribute from the XML that
 * was processed in {@link #onCreate}.
 *
 * @return The view if found or null otherwise.
 */
public View findViewById(int id) {
    return getWindow().findViewById(id);
}

又是到了window中,看下window中的方法

public View findViewById(int id) {
    return getDecorView().findViewById(id);
}

是调用了getDecorView的findViewById,也就是调用了view的findViewById,我们来看下view类中

public final View findViewById(int id) {
    if (id < 0) {
        return null;
    }
    return findViewTraversal(id);
}

protected View findViewTraversal(int id) {
    if (id == mID) {
        return this;
    }
    return null;
}

到这我们就疑惑了,直接判断了id是否为view的id,是的话就返回。怎么也应该有一个循环或者递归查找啊,什么都没有。
这时我们来看下,mID是怎么初始化的

 ....
 case com.android.internal.R.styleable.View_id:
                 mID = a.getResourceId(attr, NO_ID);
                 break;
...

喔,这个id就是我们在xml中设置的id。那会不会在ViewGroup中进行查找的呢?来看下

protected View findViewTraversal(int id) {
    if (id == mID) {
        return this;
    }

    final View[] where = mChildren;
    final int len = mChildrenCount;

    for (int i = 0; i < len; i++) {
        View v = where[i];

        if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
            v = v.findViewById(id);

            if (v != null) {
                return v;
            }
        }
    }

    return null;
}

果然, ViewGroup重写了View的findViewTraversal()方法,遍历了自己的child的findViewById方法,如果找到了返回View自身。
ok,到现在我们就理解了view是怎么findViewById的了。

总结

上面我们介绍了,Activity setContentView和findViewById的流程,是不是又多了一层理解呢,接下来我们会介绍,LayoutInflater是怎么把资源文件inflate到view中的,还有view的加载机制。如果有漏掉的之后再进行补充