源码学习《3》Layout.xml 的解析和 xml 标签生成 View 对象的过程(App 换肤原理 1)

今天要学习源码的两个问题:

  1. Layout.xml布局是怎么加载解析的
  2. Layout.xml中的 view 标签又是怎么被转化成对象的

针对这两个问题引出源码学习的流程,带着问题去看源码。

总体流程:

问题 1 布局是怎么加载的 :

首先我们最熟悉的代码肯定是:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

那就让我们从 setContentView(R.layout.activity_main); 入手。进入方法发现:

  @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

进入到 AppCompatDelegateImplV9 这是个为委托类,不是今天的重点,setContentView(int resId) 中地一句就是 ensureSubDecor(); 这个方法,由名字可知,创建sub decor view对象的。进入该方法 继续~

   private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            // 创建 sub decor
            mSubDecor = createSubDecor();

            applyFixedSizeWindow();
            ...省略...
            }
        }
    }

首先会进入 createSubDecor() 创建对象 继续 ~

private ViewGroup createSubDecor() {
        
        ..判断主题..省略...
        // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();

        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;

        ....省略..根据条件传教 subdecor.......
                // Now inflate the view using the themed context and set it as the content view
                subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);

                mDecorContentParent = (DecorContentParent) subDecor
                        .findViewById(R.id.decor_content_parent);
                mDecorContentParent.setWindowCallback(getWindowCallback());

               

        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);

      ...省略....

        return subDecor;
    }

首先是设置主题,然后 mWindow.getDecorView(); 确保decor view 已经创建,接下来根据不同的条件创建不同的 sub decor ,subdecor其实是一个 toolbar + framelayout 布局,content_parent 其实就是我们自己布局的父布局。                                             接下里就是 mWindow.setContentView(subDecor); 把subdecor传给window中的decorview。

先看 mWindow.getDecorView();

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // 创建 decorView对象
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            // 创建 decorview 内部的 contentparent布局对象
            mContentParent = generateLayout(mDecor);
              ...省略...

到这里首先是通过generateDecor(-1); 创建decorview的对象,然后在generateLayout(mDecor); 创建 内部的contentparent 对象。

然后decorView和contentparent对象创建完成之后,使用 mWindow.setContentView(subDecor);  把subdecor 对象addview放入contentparent中,代码

public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           
        } else {
            mContentParent.addView(view, params);
        }
        ...省略..
    }
setContentView(其实就是把我们创建的subdecor 对象add进入 DecorVIew中的 contentparent中去。

然后代码又回到 setContentView(int resId) 

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

这时候的mSubDecor对象已经被我们创建并且被放入window 的 decorview中去。然后把我们的布局 给 加入到 subdecor中。

问题1 我们就有答案了

接下来看问题 2 ,我们布局被加载到 系统的decorview中 后是怎么被实例化的...通过上面的代码我们能看到

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

我们的布局被传进去了,接下来就跟踪 layoutinflater。

问题 2 加载了我们的布局xml之后, tag标签又是怎么被创建成对象的呢?

通过上述我们知道我们要跟踪 LayoutInflater 的 inflate() 方法。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        // 把 layoutRes 转换成 parser 解析器
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

首先就是根据layout id 获取对应的parser解析器 res.getLayout(resource); 然后 inflate(parser, root, attachToRoot); 解析布局。

先看下 res.getLayout(resource); 最后会调到 loadXmlResourceParser() 中:

    XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
            throws NotFoundException {
          // 得到 TypedValue 实例
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            // 根据 layout id 把布局信息 赋值给 TypedValue 对象 
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                // 根据 layout 信息 返回该布局的 parser 解析器
                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
    .....省略......
    }

通过 impl.getValue(id, value, true); 根据 layout id 把layout 布局信息赋值给 typedvalue 对象,最终调到 native 方法中。

    final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        synchronized (this) {
            final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs);
       ........省略...........
        }

最后调用下述方法把 parser 返回去

                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);

这里就是根据被赋值的 value 对象创建对应layout id 的 parser。

最后又回到了 LayoutInflater的  inflate() 方法中 

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        
              ........省略.............
                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
                    // 根据 tag name 创建对应的 view 对象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                   ......省略.....

            return result;
        }
    }

通过上述代码我们知道 <merge /> 和 <include /> 是不能作为 root 根布局的。

然后就是  final View temp = createViewFromTag(root, name, inflaterContext, attrs);  创建对象

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
       
        try {
            View view;
            if (mFactory2 != null) {
                // 回调创建 view 对象
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }


            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                       // 创建 view 对象,如果为空
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        ..........省略......
    }

然后就是通过 

view = mFactory2.onCreateView(parent, name, context, attrs); 回调创建对象,这个回调其实 是 AppCompatDelegateImplV9 中的 onCreateView(parent, name, context, attrs) 方法

这个方法待会说,如果这个方法返回来的view 依然是 null 就会走到下面的判断

  if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }

这个方法中会根据 当前 view 是否包含 “.” 来 通过 反射创建 view 对象。

 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        ........省略..........
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);

                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }
            
            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            // 反射创建 view 对象
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            mConstructorArgs[0] = lastContext;
            return view;
    }

回过头 让我们继续分析 AppCompatDelegateImplV9 中的 onCreateView(parent, name, context, attrs) 方法。

    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }

这个方法最后会走到 createView(parent, name, context, attrs);

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {

              ........省略.......
                try {
                    Class viewInflaterClass = Class.forName(viewInflaterClassName);
                    mAppCompatViewInflater =
                            (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                    .newInstance();
                } catch (Throwable t) {
                    Log.i(TAG, "Failed to instantiate custom view inflater "
                            + viewInflaterClassName + ". Falling back to default.", t);
                    mAppCompatViewInflater = new AppCompatViewInflater();
                }
            }
        }

            .........省略.....

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

这个方法有两个地方1是创建了 AppCompatViewInflater 对象,2 调用了 AppCompatViewInflater 对象的 createView 方法

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
        

        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;
            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);
        }

        return view;
    }

根据这个方法可以看到,他会根据 name 判断当前是那个 view 然后 调用createxxx()方法创建 view,其中一个例子:

    @NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }

可以看到这里是 new 出来的,AppCOmpat XXX view 是谷歌做的兼容。

基本上整个流程就结束了,我们看到了 我们的view 创建的地方。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WangRain1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值