聊聊setContentView

前言

setContentView应该是我们刚开始使用Android 就使用的Api了 来看一下setContentView具体实现

先看一下setContentView时序图

时序图.png

解释一下几个类的作用

  • AppCompatDelegateImpl

    AppCompatActivity的代理实现类,AppCompatActivity的具体实现会交由它实现

  • LayoutInflater

    我用google翻译了一下 布局充气机👏 感觉有点gaygay的 这个类的作用就是解析xml 遍历创建view

  • Factory2

    这个接口只有一个方法onCreateView 顾名思义 就是创建view AppCompatDelegateImpl就继承了这个接口 我们可以实现这个接口来创建我们需要的view 比如AppCompatDelegateImpl就会将所有的TextView转换为AppCompatTextView 一会可以看一下代码

接下来上一下源码🤠 全都以AppCompatActivity为例哦

onCreate

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }

我们看到 AppCompatActivity的操作都是交由代理类来实现
重点看一下installViewFactory()

 @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);//1
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");//2
            }
        }
    }

我们看注释1的地方 发现在OnCreate方法中 会默认设置一个Factory2对象 所以我们需要在Activity.OnCreate之前设置Factory2对象 否则就会出现注释2的报错

setContentView

今天的重头戏 我们看一下上面的时序图 大致的流程其实就是解析xml 然后反射生成view 具体根据时序图 我们来看一下源码分析

我们看到 setContentView 完全都是交由delegate实现

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

    //delegate
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        //通过LayoutInflater和resId 创建View
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

之前的时序图有说明 delegate会通过LayoutInflater创建View

    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;
        }
        //生成xml解析器
        XmlResourceParser parser = res.getLayout(resource);
        try {
            //1. 通过反射生成view
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

上面这段代码会将xml文件进行解析 然后通过inflate方法创建view 并返回 我们看一下下面的部分精简代码

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            .......
            try {
                    //将parser前进到第一个START_TAG
                advanceToRootNode(parser);
                final String name = parser.getName();

                //如果是merger标签
                if (TAG_MERGE.equals(name)) {
                    ......
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //1.根据tag生成view Tag就是我们写在xml的带包名的标签 比如TextView
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        //设置LayoutParams
                        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);
                        }
                    }
                    
                    // Inflate all children under temp against its context.
                    // 递归实例化子View 这里也会根据include等标签 调用不同方法 大家可以自己看一下
                    rInflateChildren(parser, temp, attrs, true);

                    //setContentView的话 会将View 添加到android.R.id.Content中
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                   if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
                ......
            return result;
        }
    }

上面的代码我稍微精简了一下 流程主要分为3步

  1. 前进到第一个START_TAG 解析xml 生成View,但是ViewGroup都有子View
  2. 递归生成所有子View
  3. 因为是setContentView 所以attachToRoot时钟为tree 将View 添加到android.R.id.content中

我们关注的重点主要还是createViewFromTag 看下面的代码 发现createViewFromTag是交由Factory2实现

         View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ......

        try {
            //这里会交由Factory2实现 如果Factory没有处理这个Tag 那么会交由系统实现 就是下面的onCreateView和createView
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //比如TextView等不需要包名
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } 
        .......
    }

我们重点还是关注tryCreateView,onCreateView等方法大家可以自己看一下 就是反射生成view

tryCreateView会通过Factory2接口实现 还记得我们之前说 AppDelegateImpl继承了Factory2这就是AppCompatActivity对一些Tag进行了拦截创建 我们也可以自己实现Factory2来进行拦截 实现一些像换肤的功能 大家可以看一下我之前写的文章手撸动态换肤框架(一)
感觉有收获的同学点点赞呐😘

扯远了 我们看一下tryCreateView方法

    public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
              // 这里好像致敬了JAVA诞生
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        View view;
        //这里就是我们可以做的Hook点 我们以AppCompatActivity为例 看一下AppCompatActivity的实现
        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;
    }

上面方法我们发现 我们如果想Hook系统的setContentView方法的话 可以通过Factory2来实现 我们以AppCompatActivity为例 看一下AppCompatActivity Factory的实现

我们上面说过 AppCompatActivity的实现都交由AppCompatDelegate实现 具体实现类为AppCompatDelegateImpl

AppCompatDelegateImpl继承了Factory2接口 所以我们看一下AppCompatDelegateImplonCreateView伪代码

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        ......
        mAppCompatViewInflater = new AppCompatViewInflater();

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP,true, VectorEnabledTintResources.shouldBeUsed());
    }

感觉有点绕 但其实逻辑又非常清楚👍 符合单一职责 创建View都是通过LayoutInflate来实现

    final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        ......
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            ......
            }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            //注释1
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            //检查onClick 如果存在 就调用view.setonClickListener
            checkOnClickListener(view, attrs);
        }

        return view;
    }

看到AppCompatViewInflater对TextView等做了兼容处理 重点看一下注释1的地方 里面通过反射获取View 但是众所周知 反射是一个比较耗时的操作 所以我在布局优化的文章中写过 可以通过一些X2C等框架 来解决反射问题 但是可能会有一些兼容问题 需要处理一下

    private View createViewFromTag(Context context, String name, AttributeSet attrs) {
        ......
            if (-1 == name.indexOf('.')) {
                for (int i = 0; i < sClassPrefixList.length; i++) {
                    final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                    if (view != null) {
                        return view;
                    }
                }
                return null;
            } else {
                return createViewByPrefix(context, name, null);
            }
        } 
        ......
    }
    
    private View createViewByPrefix(Context context, String name, String prefix)
            throws ClassNotFoundException, InflateException {
         //先从缓存中取 避免每次都反射获取
        Constructor<? extends View> constructor = sConstructorMap.get(name);

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                //反射生成
                Class<? extends View> clazz = Class.forName(
                        prefix != null ? (prefix + name) : name,
                        false,
                        context.getClassLoader()).asSubclass(View.class);

                constructor = clazz.getConstructor(sConstructorSignature);
                sConstructorMap.put(name, constructor);
            }
            constructor.setAccessible(true);
            return constructor.newInstance(mConstructorArgs);
        } catch (Exception e) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            // try
            return null;
        }
    }

至此View已经通过反射生成了 再看一次时序图 来回顾一下整体的流程


时序图.png

总结

在学习setContentView的过程中 可以参考上面的那个时序图来分析 我们需要了解其中的几个类的职责是什么 分析清楚之后其实逻辑也就相当清楚了