聊聊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的过程中 可以参考上面的那个时序图来分析 我们需要了解其中的几个类的职责是什么 分析清楚之后其实逻辑也就相当清楚了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345