从一个控件不响应点击事件引出View Inflater以及findViewById的过程

2022-01-14/2022-01-14

一个bug引出的思考

在使用NavigationView时,通常是这样在Xml文件中设置Headlayout对应的XML文件

app:headerLayout="@layout/nav_header"

然后在Activity或者Fragment中

要通过找到NavigationView的控件后再通过NavigationView的控件才能找到headerLayout里面的控件并设计点击事件

如果直接使用findViewById然后去设置点击事件会出现该控件不响应点击事件的情况发生

原因

1.findViewById的原理

首先看看findViewById的原理

#AppCompatActivity    
@Override
    public <T extends View> T findViewById(@IdRes int id) {
        return getDelegate().findViewById(id);
    }

getDelegate()返回了AppCompatDelegateImpl

#AppCompatDelegateImpl
	@Nullable
    @Override
    public <T extends View> T findViewById(@IdRes int id) {
        ensureSubDecor();
        return (T) mWindow.findViewById(id);
    }

#Window
    public <T extends View> T findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

DecorView是一个Activity的根View,在DecorView类中没有findViewById方法,那么肯定在它的父类里面,Decor继承自FrameLayout,FrameLayout继承自ViewGroup,ViewGrouo继承自View,最终在View类中找到了findViewById方法

#View 
    public static final int NO_ID = -1;
	@Nullable
    public final <T extends View> T findViewById(@IdRes int id) {
        if (id == NO_ID) {
            return null;
        }
        return findViewTraversal(id);
    }
#View
	protected <T extends View> T findViewTraversal(@IdRes int id) {
        if (id == mID) {
            return (T) this;
        }
        return null;
    }

如果是一个View被调用findViewTraversal就会停止递归调用,如果查询的是自己就返回this

在ViewGroup中重写了findViewTraversal方法

#ViewGroup
		@Override
    protected <T extends View> T findViewTraversal(@IdRes int id) {
        if (id == mID) {
            return (T) 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 (T) v;
                }
            }
        }

        return null;
    }

可以看到首先拿到了ViewGroup的所有子View,然后遍历子View再去调用子View的findViewById方法

也就是说Android使用过dfs深度优先搜索的方式从DecorView开始通过Id搜索到对应的View

2.Android LayoutInflater的原理

在Activity中直接调用findViewById是通过从以DecorView为根节点的View树dfs深度优先搜索查询来找到对应的View的,那如果我们所要找的View不在这颗View树上那不就找不到了吗?从而不就会导致点击事件失效?我们来看看Android是怎么生成这颗View树的

从MainActivity的setContentView看起

#MainActivity
setContentView(R.layout.activity_main);

//最终会走到到AppCompatDelegateImpl的setContentView方法
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

ensureSubDecor()里面创建了DecorView并且将DecorView与Window进行了绑定

重点在

LayoutInflater.from(mContext).inflate(resId, contentParent);

在这里把我们的resId传了进去

public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

可以看到这里通过context.getSystemService获取系统服务拿到了LayoutInflater

我们接下来看它的inflate方法

inflate有多种重载,最终都会调用到一个Inflate方法

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

通过

XmlResourceParser parser = res.getLayout(resource);

我们可以知道Android是通过PULL方式解析布局XML文件的

下面看到最终的inflate方法

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                advanceToRootNode(parser);
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

可以看到通过createViewFromTag创建了View

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        try {
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

createViewFromTag由调用了tryCreateView方法并且通过反射最终完成了View的创建

public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    //这个mFactory2可以帮助我们Hook创建View的过程
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    return view;
}

创建完根View后,又会递归不断创建最终完成View树的建立

rInflateChildren(parser, temp, attrs, true);

从上面的分析可以得出View树的建立是通过LayoutInflater 通过PULL解析XML布局文件然后递归建立的,那如果XML布局文件的节点中没有这个View,那这个View岂不是就不会被插入在View树中?从而不就无法通过以DecorView为根节点的这颗View树找到这个节点

3.NavigationView相关源码

来看看NavigationView和headerLayout相关联的源码

在NavigationView的构造方法里

if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
      inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
    }

  public View inflateHeaderView(@LayoutRes int res) {
    return presenter.inflateHeaderView(res);
  }
#NavigationMenuPresenter
public View inflateHeaderView(@LayoutRes int res) {
    View view = layoutInflater.inflate(res, headerLayout, false);
    addHeaderView(view);
    return view;
  }
public void addHeaderView(@NonNull View view) {
    headerLayout.addView(view);
    // The padding on top should be cleared.
    menuView.setPadding(0, 0, 0, menuView.getPaddingBottom());
  }
headerLayout =
          (LinearLayout)
              layoutInflater.inflate(R.layout.design_navigation_item_header, menuView, false);

总结

给NavigationView添加headerLayout是通过在NavigationView的属性里设置headerLayout的xml文件,而在Activity里使用findViewById是去dfs深度优先搜索以DecorView为根节点的View树,View是通过LayoutInflater创建的,LayoutInflater通过递归解析XML文件中的节点最终得到View树,而headerLayout不属于该XML文件的节点因此不存在在以DecorView为根节点的View树上,因此查找不到。如果要找headerLayout里面的控件,要先通过找到NavigationView后再通过NavigationView找到headerLayout,在调用headerLayout的findViewById方法


标题:从一个控件不响应点击事件引出View Inflater以及findViewById的过程
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2022/01/14/1648378541971.html

评论
发表评论