findViewById 是 Android UI 设计里经常调用的一个方法,根据传入的 id 查找并返回对应的 view 对象。那么 Android 是如何去 find 一个 view 的呢,本文结合官方源码分析 findViewById 的原理。以下源码均来自 Android 7.1.1 (API 25) SDK。
findViewById 的实现
如果从一个 Activity 里调用 findViewById,则会跳到 Activity 的方法,代码如下(Activity.java 第 2322 行)
/**
* 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.
*/
@Nullable
public View findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
代码很简单,直接跳到 Window 部分,代码如下(Window.java 第 1261 行)
/**
* Finds a view that was identified by the id attribute from the XML that
* was processed in {@link android.app.Activity#onCreate}. This will
* implicitly call {@link #getDecorView} for you, with all of the
* associated side-effects.
*
* @return The view if found or null otherwise.
*/
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
然后跳到 View 部分。**如果是从一个 View / ViewGroup findViewById 则是直接从此处开始。**代码如下(View.java 第 19366 行)
/**
* Look for a child view with the given id. If this view has the given
* id, return this view.
*
* @param id The id to search for.
* @return The view that has the given id in the hierarchy or null
*/
@Nullable
public final View findViewById(@IdRes int id) {
if (id < 0) {
return null;
}
return findViewTraversal(id);
}
调用 findViewTraversal
来查找,但是此处对于 View 及 ViewGroup(继承于 View) 却有着不同的实现。
- 对于 View 比如 ImageView,判断 id 匹配则返回自己,否则返回 null。代码如下(View.java 第 19326 行)
/**
* {@hide}
* @param id the id of the view to be found
* @return the view of the specified id, null if cannot be found
*/
protected View findViewTraversal(@IdRes int id) {
if (id == mID) {
return this;
}
return null;
}
- 对于 ViewGroup 比如 LinearLayout,在他的子 View 里遍历查找 id 匹配的 View 并返回它。代码如下(ViewGroup.java 第 3942 行)
/**
* {@hide}
*/
@Override
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 的 findViewTraversal
代码可以得知 “find” 一个 View 的方法是简单的遍历。如果子 View 也是 ViewGroup 例如一个 LinearLayout 里有一个 LinearLayout,则会在 v.findViewById(id)
调用到子 ViewGroup 的 findViewTraversal
完成一个递归遍历查找的过程。下面的代码是简单的效率测试,布局中只有一个 ImageView,数据源于主流 CPU 手机的运行结果
long begin = System.nanoTime();
ImageView imageView = (ImageView) findViewById(R.id.main_imageview);
long time = System.nanoTime() - begin;
// time 的值为: 27055,30365,36562,33750,45902... 约为 20-50 微秒之间
在 Android 8.0(API 26) 中的变化
如果使用 Android 8.0 SDK 编译旧程序你可能会发现一条类似的提示:Casting ‘findViewById(R.id.main_imageview)’ to ‘ImageView’ is redundant 说类型转换是多余的,因为 API 26 已经将 findViewById 改成了这样
public <T extends View> T findViewById(int id)
所以编译的 SDK 版本更新到 26 后,即 compileSdkVersion 26
,完全可以吧 findViewById 前面的类型转换去除掉。