setContentView和findViewById的源码解析
文章目录
最近在面试的时候有碰到这样一个问题,就是“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();
}
可以看到里面做了两件事:
- 获取Window对象,并调用Window的setContentView()
- 设置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()
的流程搞清楚了。
- 通过Window对象创建DecorView对象
- 在DecorView中根据Activity的theme和style创建根布局、title等
- 通过LayoutInflater将自定义的布局文件转化为View对象,添加到根布局中作为内容的FrameLayout中
- 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()
的流程相对简单很多
- 首先获取Window对象,通过该对象得到DecorView对象
- 由DecorView对象向下,通过ViewGroup.findViewById()(调用父类View方法) -> ViewGroup.findViewTraversal() -> View.findViewById() -> View.findViewTraversal() 依次遍历整个View树,找到View对象
文章作者 Dio.Ye
上次更新 2016-03-08