最近在面试的时候有碰到这样一个问题,就是“View.inflate()和View.findViewById()哪个更耗时,为什么”。通过之前对于View.inflate()源码的分析,可以知道在将布局文件转化为View对象的时候需要通过io读取文件+遍历解析XML文件+生成每一个View对象,而findViewById()只是遍历View树来获取对应的View对象。所以很明显应该是View.inflate()更为耗时。

那么,findViewById()究竟是如何通过id来获取对应的View对象的呢?我们今天从Activity.setContentView()Activity.findViewById()的源码谈起

Activity.setContentView(int layoutResID)

在使用Activity时,都会在onCreate()中通过setContentView()给Activity设置布局

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

可以看到里面做了两件事:

  1. 获取Window对象,并调用Window的setContentView()
  2. 设置ActionBar

PhoneWindow.setContentView(int layoutResID)

我们只关注前者。Window是一个抽象类,他具体的实现大多在PhoneWindow 这个实现类中

public void setContentView(int layoutResID) {
    // 初始化DecorView
    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 {
        // 通过LayoutInflater将布局文件添加到 mContentParent中
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();

    // Activity 实现了Window.Callback接口.
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

首先判断mContentParent,如果为null就初始化,否则将其所有的View移除。然后通过LayoutInflater把布局文件添加到mContentParent中。之后还获取了一个Calback对象,来进行回调。而这个Callback对象正是Activity,他实现了Callback接口,需要我们自己去实现onContentChanged()方法

PhoneWindow.installDecor()

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        mDecor.makeOptionalFitsSystemWindows();

        final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                R.id.decor_content_parent);

        if (decorContentParent != null) {
        /*
         * 省略代码,根据style和其他一些属性去设置title、icon等等
         */
       } else {
            mTitleView = (TextView)findViewById(R.id.title);
            if (mTitleView != null) {
                mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
                // 为什么需要在setContentView()之前调用requestWindowFeature()
                if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                    //...
                } else {
                    mTitleView.setText(mTitle);
                }
            }
        }
    }
}

我们知道在Android的每个页面中,都会由系统来初始化一个根布局DecorView,它是FrameLayout的子类。事实上就是在installDecor()方法里面进行的。可以看到首先调用了generateDecor()来实例化了一个DecorView,然后通过generateLayout(mDecor)生成了mContentParent。此后会通过我们给Activity设置的style和属性对DDecorView进行初始化。在这里有一点需要注意的,就是我们都知道,如果要设置Activity无标题,需要在setContentView()之前调用requestWindowFeature(FEATURE_NO_TITLE),在源码中我们可以找到答案。

PhoneWindow.generateDecor()

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

protected ViewGroup generateLayout(DecorView decor) {
     // Apply data from current theme.
     TypedArray a = getWindowStyle();

     /*
      * 通过获取的theme来设置根布局
      */

      // 根据theme来获取指定的布局文件,添加到DecorView中
      View in = mLayoutInflater.inflate(layoutResource, null);
      decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
      mContentRoot = (ViewGroup) in;

      // 不论是那种跟布局文件,都可以ID_ANDROID_CONTENT来获取一个contentParent
      // 就是我们可以自己去填充内容的FrameLayout
      ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

      /*
       * 省略
       */

      return contentParent;
}

generateDecor()仅仅是返回一个DecorView对象。而在generateLayout()中,可以看到通过TypedArray去获取我们在XML中给Activity定义的theme和style,根据这些属性决定最终的使用是哪个布局。最终返回的是布局文件中id为ID_ANDROID_CONTENT的FrameLayout对象。

小结

到这里,我们基本把Activity.setContentView()的流程搞清楚了。

  1. 通过Window对象创建DecorView对象
  2. 在DecorView中根据Activity的theme和style创建根布局、title等
  3. 通过LayoutInflater将自定义的布局文件转化为View对象,添加到根布局中作为内容的FrameLayout中
  4. LayoutInflater.inflate()依次向下遍历,直到生成整个View树

Activity.findViewById(int id)

public View findViewById(@IdRes int id) {
    return getWindow().findViewById(id);
}

看到findViewById()的源码我想你也会会心一笑,他跟setContentView()真的很像,同样是由Window这个类区来实现

Window.findViewById()

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

通过上面我们对setContentView()的分析,可以直到getDecorView()其实就是返回页面上最顶端的ViewGroup对象,从上向下去findViewById(int id)找到指定的子控件

View.findViewById()

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

View.findViewById()实际上是调用findViewTraversal()方法。这个方法就是根据id决定返回的是View自身还是null

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

我们已经知道,DecorView是FrameLayout的子类。所以向下遍历findViewById()的核心应该实在ViewGroup这个View的子类中。

ViewGroup.findViewTraversal(int id)

protected View findViewTraversal(@IdRes 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首先也是判断自身的mID与参数id是否相等,来决定是否需要返回自身。之后,可以看到在ViewGroup中维护了一个View[]数组,用来存储所有的子控件。遍历这些子控件,再调用子控件的findViewById()向下遍历,直到找到指定id的View对象。

小结

Activity.findViewById()的流程相对简单很多

  1. 首先获取Window对象,通过该对象得到DecorView对象
  2. 由DecorView对象向下,通过ViewGroup.findViewById()(调用父类View方法) -> ViewGroup.findViewTraversal() -> View.findViewById() -> View.findViewTraversal() 依次遍历整个View树,找到View对象