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 前面的类型转换去除掉。