【面向面试学习】setContentView(int resId)究竟干了什么

677 阅读9分钟

前言

不要慌拢共分9步,逻辑很垂直线性,递归逻辑不多

1. 从AppCompatActivity的setContentView作为入口

//AppCompatActivity.java
@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}
//Activity将setContentView委派给了代理AppCompatDelegate去执行
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

2. 进入AppCompatDelegate继续追查setContentView

//AppCompatDelegate.java
public static AppCompatDelegate create(@NonNull Activity activity,
        @Nullable AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, callback);
}
//AppCompatDelegate只是个虚类,实际由AppCompatDelegateImpl去实现setContentView

3. 进入AppCompatDelegateImpl后找到setContentView的具体实现

//AppCompatDelegateImpl.java
@Override
public void setContentView(int resId) {
    ensureSubDecor();//将Activity attach到window上 并拿到父容器mSubDecor
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

4. 加载resId的艰巨任务交给了LayoutInflater

//LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}
//XmlResourceParser登场 XMl的解析读取即将开始
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) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

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;

            int type;
            //如果 parser.next() 既不是开始 也不是结束 那就一直next,直到找到符合要求的标签
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
            }
            //如果 parser.next()到最后没有找到开始标签,就证明这个xml是不合法的
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            //经过上面的while会将parser的指针指向xml根节点的开始标签
            final String name = parser.getName();//获取根节点标签名字
            //遇到Merge标签,就去将merge指向的xml引进来
            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 {
                //这是主流程!!!
                //第一步,通过createViewFromTag将xml根布局标签对应的根View创建出来
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;
                //第二步,根据xml里的配置生成LayoutParams,并设置给根View
                if (root != null) {
                
                    // 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);
                    }
                }
                //第三步,将根View下属的所有子View从xml里创建出来。核心技术在这里
                rInflateChildren(parser, temp, attrs, true);
                //截至到这里xml里描述的所有view控件就已经被创建并载入内存
                
                //如果外部传入的父容器不为空,就把根View添加进去
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                //如果外部传入的父容器为空,那么根View直接作为结果返回
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        return result;
    }
}

5. 接下来分析核心方法rInflateChildren,xml如何转化为View对象?

//LayoutInflater.java
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
//很显然rInflateChildren仅仅是对rInflate做了一个包装,上一步解析根View时,遇到merge标签就是直接调的rInflate,接下来看rInflate
void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    //Xml构建的UI其实是一个树形结构,这个depth代表当前遍历到的标签所位于的高度,是随着遍历随时变化的一个值,根标签为0,以此类推到叶子节点,每一层增加1
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        //这个循环里的处理,都是从开始标签开始的,所以不是的话直接continue    
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
        //如果遇到<requestFocus>标签做出相应的设置,然后通过consumeChildElements忽略<requestFocus>下的所有子标签,也就是说,不管你<requestFocus>下有多少子tag一概不管。consumeChildElements是个static方法,可能别处也会用到,核心就是xml遍历时忽略指定标签下的所有子标签
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
        //遇到<tag>标签,提取其k v值并通过parent.setTag(k, v)设置给parent
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
        //<include />不能作为根标签
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
        //遇到<include />标签提取其指向的布局xml,最后还是调用rInflateChildren去解析include的xml,这块有点递归的意思    
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
        //这块很明显,<merge />只能是一个xml的根标签,不得作为子标签出现
            throw new InflateException("<merge /> must be the root element");
        } else {
        //createViewFromTag根据此时遍历到的标签创建View,然后递归调用rInflateChildren继续创建该标签的子标签,递归结束后添加到父容器parent
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

6. 根据上面的分析rInflateChildren就是遍历xml,递归调用rInflateChildren,通过createViewFromTag创建发现的每一个View,接下来分析createViewFromTag函数

//LayoutInflater.java

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
}

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

    // 设置主题theme
    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();
    }
    //对于隐藏布局BlinkLayout的解析,这玩意有点彩蛋的意思,正经人谁用他,哈哈
    //使用这个布局的页面每隔0.5秒是闪一次
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    try {
        View view;
        //View创建的核心就是这几个Factory,待会扒一扒他
        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);
        }
        //如果走到这里view还是空,说明是上面三个Factory都处理不了的View
        //这是时候就要根据标签名字直接生成了
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                //标签名不含包名,说明是系统控件,"android.view."+name,通过这样组装出来的路径,反射创建这个View,最终也是通过else里的createView实现的
                    view = onCreateView(parent, name, attrs);
                } else {
                //这里的标签是含有包名的全路径,一般是第三方或者自定义View,也是根据这个路径反射创建View
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

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

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

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

7. 上一步分析了createViewFromTag,可知主要是通过Factory和createView来最终创建View的,createView好理解根据类的全路径名,反射构造函数创建,那么这俩Factory干嘛的。

//LayoutInflater.java
//1.这两个Factory注入都是位于LayoutInflater.java
//2.这两个Factory只能注入一次
//3.如果你想注入第二次,可以在注入前反射修改mFactorySet为false,然后再注入
//4.从createViewFromTag的分析可知,Factory2优先级高于Factory1
public void setFactory(Factory factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

/**
 * Like {@link #setFactory}, but allows you to set a {@link Factory2}
 * interface.
 */
public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

//此时不得不提LayoutInflaterCompat.java里的setFactory
//1.这个是LayoutInflaterCompat是LayoutInflater的一个包装类
//2.这个方法传入一个普通的LayoutInflaterFactory
//3.当Build.VERSION.SDK_INT >= 21时将普通Factory直接包装为Factory2
//4.说明官方已经不建议或者废弃了Factory,转而使用Factory2

@Deprecated
public static void setFactory(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
    if (Build.VERSION.SDK_INT >= 21) {
        inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
    } else {
        final LayoutInflater.Factory2 factory2 = factory != null
                ? new Factory2Wrapper(factory) : null;
        inflater.setFactory2(factory2);

        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
            // We will now try and force set the merged factory to mFactory2
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // Else, we will force set the original wrapped Factory2
            forceSetFactory2(inflater, factory2);
        }
    }
}

8. 上一步得知Factory已经处于过期状态,接下来直接分析Factory2

那就先查找一下调用setFactory2的在哪里,调用的地方很多,但是惊喜的看到了,上面分析里第3步提到的AppCompatDelegateImpl

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } 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");
        }
    }
}

public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
//Factory2就是一个接口 上面的installViewFactory里的LayoutInflaterCompat.setFactory2(layoutInflater, this)说明onCreateView的实现就在上面分析里第3步提到的AppCompatDelegateImpl里

@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
  
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, 
            true, 
            VectorEnabledTintResources.shouldBeUsed() 
    );
}

//目标锁定mAppCompatViewInflater也就是AppCompatViewInflater.java,所有谜题的答案就在这里了

9. 闲言碎语不要讲,直接进入AppCompatViewInflater的createView方法

  • 大家直接看就能秒懂了,多余的话其实不用说
  • 能一直追到这里的,都是好奇心爱好者,哈
  • 这是Fatory2,不用看Factory1大概也是这意思
final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;

    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        // We then apply the theme on the context, if specified
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }

    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    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;
        case "Spinner":
            view = createSpinner(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageButton":
            view = createImageButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckBox":
            view = createCheckBox(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RadioButton":
            view = createRadioButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckedTextView":
            view = createCheckedTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "AutoCompleteTextView":
            view = createAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "MultiAutoCompleteTextView":
            view = createMultiAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RatingBar":
            view = createRatingBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "SeekBar":
            view = createSeekBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ToggleButton":
            view = createToggleButton(context, attrs);
            verifyNotNull(view, name);
            break;
        default:
            // The fallback that allows extending class to take over view inflation
            // for other tags. Note that we don't check that the result is not-null.
            // That allows the custom inflater path to fall back on the default one
            // later in this method.
            view = createView(context, name, attrs);
    }

    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.
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}

10. 总结

  • setContentView是所有Android开发者会的第一个函数,他是那么亲切,又陌生,亲切用的次数实在是太多了,陌生是因为我们从来没有好好关注过他
  • Inflater的setFactory2函数可以从xml解析这个级别拦截所有View创建过程,功能无比强大
  • 自定义Factory2是Android换肤功能三大技术流派之一的底层核心逻辑
  • 从静态xml文件到手机屏幕上酷炫效果的飞跃,就是今天分析的主题

以上分析基于appcompat-1.1.0-source.jar