Android技术知识程序员首页投稿(暂停使用,暂停投稿)

Android-LayoutInflater布局文件解析过程分析

2017-02-17  本文已影响510人  良秋

备注:

本篇文章所引用的源码版本:android-sdk-21

转载请注明出处:http://blog.csdn.net/a740169405/article/details/54580347

简述:

简单的说,LayoutInflater就是是一个用来解析xml布局文件的类。该篇文章将对LayoutInflater类进行分析,内容包括:
1. LayoutInflater在哪里创建
2. 如何获取LayoutInflater对象
3. 视图的创建过程(xml转换成View的过程)
4. inflate方法的两个重要参数(root、attachToRoot)分析


LayoutInflater的来源:

LayoutInflater和其他系统服务一样,也是在ContextImpl类中进行注册的,ContextImpl类中有一个静态代码块,应用程序用到的系统服务都在这进行注册:

class ContextImpl extends Context {
    static {
        // ...

        // 注册ActivityManager服务
        registerService(ACTIVITY_SERVICE, new ServiceFetcher() {
                public Object createService(ContextImpl ctx) {
                    return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
                }});
        // 注册WindowManager服务
        registerService(WINDOW_SERVICE, new ServiceFetcher() {
                Display mDefaultDisplay;
                public Object getService(ContextImpl ctx) {
                    Display display = ctx.mDisplay;
                    if (display == null) {
                        if (mDefaultDisplay == null) {
                            DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                    getSystemService(Context.DISPLAY_SERVICE);
                            mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                        }
                        display = mDefaultDisplay;
                    }
                    return new WindowManagerImpl(display);
                }});
        
        // ....

        // 注册LayoutInflater服务
        registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
                public Object createService(ContextImpl ctx) {
                    return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
                }});

        // ...其他服务的注册,不一一列举,有兴趣可以自己看源码
    }

    // ...其他代码

    // 存储所有服务的ServiceFetcher集合
    private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
            new HashMap<String, ServiceFetcher>();

    private static void registerService(String serviceName, ServiceFetcher fetcher) {
        if (!(fetcher instanceof StaticServiceFetcher)) {
            fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
        }
        SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
    }
}

从代码中可以发现,除了LayoutInflater的注册,还有我们常见的WindowManager、ActivityManager等的注册。所有的注册都调用了静态方法:registerService,这里所有的服务并不是在静态代码块中直接创建,而是采用饥渴式方法,只创建了对应服务的获取器ServiceFetcher对象。在真正使用特定服务的时候才创建,SYSTEM_SERVICE_MAP是一个静态的集合对象,存储了所有服务的获取器(ServiceFetcher)对象,map的键是对应服务的名称。只需要调用获取器(ServiceFetcher)的getService(Context context)方法既可以获取对应的系统服务。

我们只关注LayoutInflater的获取器(ServiceFetcher)是如何实现的,其getService(Context context);方法调用了com.android.internal.policy.PolicyManager#makeNewLayoutInflater(Context context)

public static LayoutInflater makeNewLayoutInflater(Context context) {
    return new BridgeInflater(context, RenderAction.getCurrentContext().getProjectCallback());
}

这里提一下,上面代码是android-sdk-21版本的源码,创建了一个BridgeInflater对象,如果是android-sdk-19及以下的源码,PolicyManager#makeNewLayoutInflater方法应该是:

public static LayoutInflater makeNewLayoutInflater(Context context) {
    return sPolicy.makeNewLayoutInflater(context);
}

接着调用了com.android.internal.policy.impl.Policy#makeNewLayoutInflater(Context context)方法:

public LayoutInflater makeNewLayoutInflater(Context context) {
    return new PhoneLayoutInflater(context);
}

也就是说android-sdk-19及以下的版本是创建一个PhoneLayoutInflater对象。

BridgeInflate和PhoneLayoutInflater都是继承自LayoutInflater,实现了解析xml布局的API,将会在后面分析xml布局文件解析过程时用上。这里不讨论两者的实现以及区别。


获取LayoutInflater对象:

按照上面的逻辑,LayoutInflater不需要我们自己new,framework层已经帮我们创建好,自然也会也会提供API供开发者获取LayoutInflater对象。

方式一:

既然LayoutInflater是在ContextImpl中注册的,Context也提供了接口来获取LayoutInflater服务,也就是Context#getSystemService(String name);方法:

@Override
public Object getSystemService(String name) {
    ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
    return fetcher == null ? null : fetcher.getService(this);
}

该方法从SYSTEM_SERVICE_MAP集合内取出对应服务的获取器ServiceFetcher,并调用其getService方法来获取服务,首次调用的时候,将会调用到ServiceFetcher类的createService方法来创建一个LayoutInflater对象,之后将会返回已经创建好的对象。

所有的其他获取LayoutInflater对象的方式,都将调用到Context#getSystemService(String name);方法,我们继续往下看看其他方式是如何获取的。

方式二:

通过LayoutInflater#from(context)方法来获取:

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(String name);方法,并将LayoutInflater服务名称传递进去。

方式三:

如果在Activity内,可以通过Activity#getLayoutInflater();方法获取LayoutInflater,该方法是Activity封装的一个方法:

@NonNull
public LayoutInflater getLayoutInflater() {
    return getWindow().getLayoutInflater();
}

Activity里的getWindow返回的是一个PhoneWindow对象,接着看PhoneWindow#getLayoutInflater();

@Override
public LayoutInflater getLayoutInflater() {
    return mLayoutInflater;
}

返回了一个LayoutInflater对象,其初始化是在PhoneWindow的构造方法里:

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}

其最终调用了方式二中的LayoutInflater#from(Context context);方法。


布局解析过程

接着,分析LayoutInflater是如何将一个xml布局文件解析成一个View对象的。涉及到以下内容:

  1. LayoutInflater#inflate(...);的四个重构方法
  2. LayoutInflater#inflate(...);是如何解析视图的

LayoutInflater#inflate(...);的四个重构方法

通过LayoutInflater对外提供的四个inflate重构方法来入手视图解析流程:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root);
public View inflate(XmlPullParser parser, @Nullable ViewGroup root);
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);

调用关系如下:

  1. 第一个重构方法最后调用了第三个重构方法,第三个重构方法最后调用了第四个重构方法。
  2. 第二个重构方法最终调用了第四个重构方法

第一个:

public View inflate(int resource, ViewGroup root) {
    // 调用第三个重构方法
    return inflate(resource, root, root != null);
}

第二个:

public View inflate(XmlPullParser parser, ViewGroup root) {
    // 调用第四个重构方法
    return inflate(parser, root, root != null);
}

第三个:

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }
    // 通过resource资源文件获取xml解析器
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        // 调用第四个重构方法
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

第四个:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    // 省略内容,后面分析
}

真正开始布局的解析流程的是第四个重构方法,也就是说我们只要分析第四个重构方法的流程就能知道xml布局文件是如何被解析的。

LayoutInflater#inflate(...);是如何解析视图的

视图的解析过程可以总结成:

  1. 使用XmlPullParser遍历xml文件内的所有节点
  2. 在遍历到某一节点时,根据节点名字生成对应的View对象
  3. 在生成View对象时,将AttributeSet以及Context传递给View对象的构造方法,在构造方法中,View或者其子类将通过AttributeSet获取自身的属性列表,并用来初始化View。如background等属性。

在分析视图的解析过程之前,需要先了解什么是XmlPullParser,他是第二个和第四个重构方法的参数,XmlPullParser是一个接口,定义了一系列解析xml文件的API。

java中解析xml的常用方式有DOM和SAX两种方式,pull解析是android提供的一种。

这里引用一段对pull方式的描述:

在android系统中,很多资源文件中,很多都是xml格式,在android系统中解析这些xml的方式,是使用pul解析器进行解析的,它和sax解析一样(个人感觉要比sax简单点),也是采用事件驱动进行解析的,当pull解析器,开始解析之后,我们可以调用它的next()方法,来获取下一个解析事件(就是开始文档,结束文档,开始标签,结束标签),当处于某个元素时可以调用XmlPullParser的getAttributte()方法来获取属性的值,也可调用它的nextText()获取本节点的值。

对xml解析方式的使用有兴趣可以参阅:
android解析XML总结(SAX、Pull、Dom三种方式)

那么XmlPullParser对象是如何生成的。看看重构方法三:

final XmlResourceParser parser = res.getLayout(resource);

res是Resource类对象,resource是资源文件id,看看Resource#getLayout(int id);方法的实现:

public XmlResourceParser getLayout(int id) throws NotFoundException {
    return loadXmlResourceParser(id, "layout");
}

Resource#loadXmlResourceParser(int id, String type);方法最终将会返回一个XmlBlock#Parser类型的对象:

final class XmlBlock {
    // ...
    final class Parser implements XmlResourceParser {
        // ...
    }
    // ...
}

XmlResourceParser继承自XmlPullParser、AttributeSet以及AutoCloseable(一个定义了不使用时需要关闭的接口):

public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
    
    public void close();
}

也就是说最终返回了一个XmlPullParser接口的实现类Parser,Parser类还实现了AttributeSet接口。

那么大家经常在View的构造方法里见到的AttributeSet到底什么:

Android引入了pull解析,其中XmlPullParser这个接口定义了操作pull解析方式对xml文件的所有操作接口,包括对节点的操作,对节点内的属性的操作,以及next等接口。而AttributeSet则是Android针对资源文件的特点定义的一个接口,该接口描述了对节点内的属性集的操作接口,除了getAttributeValue、getAttributeCount等一些和XmlPullParser接口相同的接口外。AttributeSet还定义了一些如getIdAttribute、getAttributeResourceValue、getAttributeBooleanValue这些pull解析方式之外的一些带有android特性的接口,相当于是对节点的属性集合的操作接口进行了拓展。

这样看来,XmlBlock#Parser类除了实现了pull解析方式自带的接口定义外。还实现了AttributeSet接口内定义的一些具有android特性的接口。

但是Parser内并未存储节点下所有的Attributes(属性)。这些属性都是存在android.content.res.TypedArray内,而如何得到TypedArray类型对象,继续往下看。

回到LayoutInflater#inflate的第四个重构方法,看看是如何使用parser这个xml解析器的。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        // ...

        // 因为parser实现了AttributeSet接口,所以这里是强转
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        
        // result是需要return的值
        View result = root;

        try {
            // 通过一个循环,寻找根节点
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
                // 如果没找到根节点,报错
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            
            // 找到了根节点,获取根节点的名称
            final String name = parser.getName();

            if (TAG_MERGE.equals(name)) {
                // 如果根节点是merge标签
                if (root == null || !attachToRoot) {
                    // merge标签要求传入的ViewGroup不能是空,并且attachToRoot必须为true, 否则报错
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                // 递归生成根节点下的所有子节点
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 根据节点的信息(名称、属性)生成根节点View对象
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                // 根节点的LayoutParams属性
                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    // 如果传入的ViewGroup不为空

                    // 调用root的generateLayoutParams方法来生成根节点的LayoutParams属性对象
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // 不需要讲根节点添加到传入的ViewGroup节点下,则将LayoutParams对象设置到根节点内
                        // 否则的话在后面将会通过addView方式设置params
                        temp.setLayoutParams(params);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                    // 开始解析所有子节点
                }

                // 解析根节点下的子节点
                rInflateChildren(parser, temp, attrs, true);

                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                    // 结束了所有子节点的解析
                }

                if (root != null && attachToRoot) {
                    // 如果传入的ViewGroup不是空,并且需要添加根节点到其下面
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    // 如果根节点为空,或者是attachToRoot为false,返回根节点
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            // ....
        } catch (Exception e) {
            // ....
        } finally {
            // ....
        }

        // return 结果(根节点或者是传入的ViewGroup)
        return result;
    }
}

这里有几个比较关键的地方,一一进行分析:

// 根据节点的信息(名称、属性)生成根节点View对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

createViewFromTag方法创建了对应节点的View对象:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        // 如果节点名字为view,则取节点下面的class属性作为名字
        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();
    }

    // 几点名称为blink的时候,创建一个BlinkLayout类对象,继承自FrameLayout。
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    try {
        View view;

        // mFactory和mFactory2是两个工厂类,可以对视图的创建进行hook,暂时不分析
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        // 和mFactory类似,暂不分析
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        // 最终会走到这,
        if (view == null) {
            // View的构造方法参数:context
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    // 如果节点名字不带".",说明是系统提供的View(Button/TextView等),走系统View的创建流程,android.view包下的
                    view = onCreateView(parent, name, attrs);
                } else {
                    // 否则则说明是自定义View,走自定义View的创建流程
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        // 返回解析出来的View
        return view;
    } catch (InflateException e) {
        // ...
    } catch (ClassNotFoundException e) {
        // ...
    } catch (Exception e) {
        // ...
    }
}

最终会调用LayoutInflater#createView方法来创建指定名字的View(调用onCreateView方法最后也会调用createView方法):

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {

    // sConstructorMap存储了所有解析过的View的构造方法Constructor
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    // 待解析的View的Class
    Class<? extends View> clazz = null;

    try {
        if (constructor == null) {
            // 缓存中没有该类型的构造方法,也就是之前没有解析过该Class类型的View,
            // 通过反射获取Constructor对象,并缓存
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
            
            // Filter这个东西是用来拦截节点解析的,
            // onLoadClass返回false的话,将会调用failNotAllowed,就是报错,不允许解析
            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            // 反射获取Constructor对象,并缓存
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        } else {
            if (mFilter != null) {
                // 如果有拦截器的话,需要通过缓存的拦截信息判断是否需要拦截解析,
                // 如果未缓存拦截信息的话,则动态从mFilter#onLoadClass中取出拦截信息
                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[] args = mConstructorArgs;
        // View的构造方法里第二个参数是AttributeSet,一个用来解析属性的对象
        args[1] = attrs;

        // View对象的真正创建
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
            // 如果是ViewStub的话,需要为其设置一个copy的LayoutInflater
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        // 返回结果
        return view;

    } catch (NoSuchMethodException e) {
        // 这个报错比较重要
        InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Error inflating class "
                + (prefix != null ? (prefix + name) : name));
        ie.initCause(e);
        throw ie;
    } catch (ClassCastException e) {
        // ...
    } catch (ClassNotFoundException e) {
        // ...
    } catch (Exception e) {
        // ...
    } finally {
        // ...
    }
}

LayoutInflater是通过反射的方式创建View,并将context以及AttributeSet对象作为参数传入。

也就是说如果用户自定义View的时候,没有重写带两个参数的构造方法的话,将会报错。代码将会走到上面NoSuchMethodException这个catch中。例如下面这个报错信息(注意注释部分):

FATAL EXCEPTION: main
Process: com.example.j_liuchaoqun.myapplication, PID: 26075
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.j_liuchaoqun.myapplication/com.example.j_liuchaoqun.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #13: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2793)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864)
    at android.app.ActivityThread.-wrap12(ActivityThread.java)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:156)
    at android.app.ActivityThread.main(ActivityThread.java:6524)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:941)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831)
 Caused by: android.view.InflateException: Binary XML file line #13: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView
 Caused by: android.view.InflateException: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView

 // 大家主要看下面这行信息,在createView(LayoutInflater.java:625)方法中反射时,提示缺少一个SlideTextView(Context context, AttributeSet set);的构造方法。
 
 Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]
    at java.lang.Class.getConstructor0(Class.java:2204)
    at java.lang.Class.getConstructor(Class.java:1683)
    at android.view.LayoutInflater.createView(LayoutInflater.java:625)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:798)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:738)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:869)
    at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:832)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:518)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:426)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:377)
    at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:255)
    at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:109)
    at com.example.j_liuchaoqun.myapplication.MainActivity.onCreate(MainActivity.java:11)
    at android.app.Activity.performCreate(Activity.java:6910)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2746)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864)
    at android.app.ActivityThread.-wrap12(ActivityThread.java)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:156)
    at android.app.ActivityThread.main(ActivityThread.java:6524)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:941)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831)

在API21中,将会调用到View的一个四个参数的构造方法,低版本API中可能只有三个构造方法,但不管如何,最后都会调用到参数最多的那个构造方法,并在该方法中对View进行初始化,而初始化的信息,都将通过AttributeSet生成的TypedArray对象来获取。

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    this(context);

    // 解析styleable.View的所有属性
    final TypedArray a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

    // ...

    // 遍历解析出来的所有属性,并设置为当前View对象
    final int N = a.getIndexCount();
    for (int i = 0; i < N; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
            case com.android.internal.R.styleable.View_background:
                // 背景
                background = a.getDrawable(attr);
                break;
            }
            // ...其他case
            default:
                break;
        }
    }

    // ...
}

这里对其构造方法进行了简化,可以看到,AttributeSet是在这里使用的,通过context.obtainStyledAttributes方法将attrs.xml下定义的View这个styable属性集解析出来,android源码中的attrs.xml文件中定义了View的所有属性:

<!-- Attributes that can be used with {@link android.view.View} or
     any of its subclasses.  Also see {@link #ViewGroup_Layout} for
     attributes that are processed by the view's parent. -->
<declare-styleable name="View">
    <!-- Supply an identifier name for this view, to later retrieve it
         with {@link android.view.View#findViewById View.findViewById()} or
         {@link android.app.Activity#findViewById Activity.findViewById()}.
         This must be a
         resource reference; typically you set this using the
         <code>@+</code> syntax to create a new ID resources.
         For example: <code>android:id="@+id/my_id"</code> which
         allows you to later retrieve the view
         with <code>findViewById(R.id.my_id)</code>. -->
    <attr name="id" format="reference" />

    <!-- Supply a tag for this view containing a String, to be retrieved
         later with {@link android.view.View#getTag View.getTag()} or
         searched for with {@link android.view.View#findViewWithTag
         View.findViewWithTag()}.  It is generally preferable to use
         IDs (through the android:id attribute) instead of tags because
         they are faster and allow for compile-time type checking. -->
    <attr name="tag" format="string" />

    <!-- The initial horizontal scroll offset, in pixels.-->
    <attr name="scrollX" format="dimension" />

    <!-- The initial vertical scroll offset, in pixels. -->
    <attr name="scrollY" format="dimension" />

    <!-- A drawable to use as the background.  This can be either a reference
         to a full drawable resource (such as a PNG image, 9-patch,
         XML state list description, etc), or a solid color such as "#ff000000"
        (black). -->
    <attr name="background" format="reference|color" />

    <!-- Sets the padding, in pixels, of all four edges.  Padding is defined as
         space between the edges of the view and the view's content. A views size
         will include it's padding.  If a {@link android.R.attr#background}
         is provided, the padding will initially be set to that (0 if the
         drawable does not have padding).  Explicitly setting a padding value
         will override the corresponding padding found in the background. -->
    <attr name="padding" format="dimension" />
    <!-- Sets the padding, in pixels, of the left edge; see {@link android.R.attr#padding}. -->
    <attr name="paddingLeft" format="dimension" />
    <!-- Sets the padding, in pixels, of the top edge; see {@link android.R.attr#padding}. -->
    <attr name="paddingTop" format="dimension" />
    <!-- Sets the padding, in pixels, of the right edge; see {@link android.R.attr#padding}. -->
    <attr name="paddingRight" format="dimension" />
    <!-- Sets the padding, in pixels, of the bottom edge; see {@link android.R.attr#padding}. -->
    <attr name="paddingBottom" format="dimension" />
    <!-- Sets the padding, in pixels, of the start edge; see {@link android.R.attr#padding}. -->
    <attr name="paddingStart" format="dimension" />
    <!-- Sets the padding, in pixels, of the end edge; see {@link android.R.attr#padding}. -->
    <attr name="paddingEnd" format="dimension" />

    <!-- 属性太多,不一一列举 -->
</declare-styleable>

当然,如果你是View的子类,也有对应的属性,比如ListView:

<declare-styleable name="ListView">
    <!-- Reference to an array resource that will populate the ListView.  For static content,
         this is simpler than populating the ListView programmatically. -->
    <attr name="entries" />
    <!-- Drawable or color to draw between list items. -->
    <attr name="divider" format="reference|color" />
    <!-- Height of the divider. Will use the intrinsic height of the divider if this
         is not specified. -->
    <attr name="dividerHeight" format="dimension" />
    <!-- When set to false, the ListView will not draw the divider after each header view.
         The default value is true. -->
    <attr name="headerDividersEnabled" format="boolean" />
    <!-- When set to false, the ListView will not draw the divider before each footer view.
         The default value is true. -->
    <attr name="footerDividersEnabled" format="boolean" />
    <!-- Drawable to draw above list content. -->
    <attr name="overScrollHeader" format="reference|color" />
    <!-- Drawable to draw below list content. -->
    <attr name="overScrollFooter" format="reference|color" />
</declare-styleable>

对应在ListView的构造方法里有:

public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    // ...

    final TypedArray a = context.obtainStyledAttributes(
            attrs, R.styleable.ListView, defStyleAttr, defStyleRes);
    
    // 从节点中获取Divider属性,如果有定义的话,设置到ListView中
    final Drawable d = a.getDrawable(R.styleable.ListView_divider);
    if (d != null) {
        // Use an implicit divider height which may be explicitly
        // overridden by android:dividerHeight further down.
        setDivider(d);
    }

    // 其他ListView提供的属性...
}

至此,xml中根节点的解析过程告一段落。

那么LayoutInflater是如何解析xml下的其他子节点的? 回过头来看LayoutInflater#inflate第四个重构方法里有一段代码:

// 解析根节点下的子节点
rInflateChildren(parser, temp, attrs, true);

该方法将会遍历View的所有子节点,并调用createViewFromTag对每一个节点进行解析,并把解析出来的View添加到父节点中。具体内如如何实现,大家可以看看源码。与xml的根节点解析类似。

inflate方法的attachToRoot(Boolean)参数

attachToRoot是inflate接收的一个参数,它有两重作用:

  1. 表示是否需要将解析出来的xml根节点add到传入的root布局中(如果root不为空的话)。
  2. 如果attachToRoot为true,则inflate方法将返回root对象,否则,将返回解析出来的xml根节点View对象。

inflate方法的root(ViewGroup)参数

如果root不为空,将会调用root的generateLayoutParams方法为xml跟布局生成LayoutParams对象。generateLayoutParams是ViewGroup中定义的方法。它的子类可以对其进行重写,以返回对应类型的LayoutParams

FrameLayout#generateLayoutParams(android.util.AttributeSet):

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new FrameLayout.LayoutParams(getContext(), attrs);        
}

RelativeLayout#generateLayoutParams(android.util.AttributeSet):

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new RelativeLayout.LayoutParams(getContext(), attrs);
}

可以发现,如果传入的root是FrameLayout类型的话,将会生成FrameLayout.LayoutParams,如果传入的root是RelativeLayout类型的话,将会生成RelativeLayout.LayoutParams。

根据这样的规律,分析下面两种情况:

  1. xml根节点定义了属性android:layout_centerHorizontal="true",而inflate方法传入的root对象为FrameLayout类型,此时android:layout_centerHorizontal将会失效,因为FrameLayout.LayoutParam对象并不支持layout_centerHorizontal属性。
  2. xml根节点定义了属性android:layout_gravity="center",而inflate方法传入的的root对象为RelativeLayout类型,此时android:layout_gravity也会失效,因为RelativeLayout.LayoutParams并不支持layout_gravity属性。
  3. 同理还需要考虑LinearLayout.LayoutParams所支持的属性与xml根节点定义的属性是否有冲突。

如果传入的root对象为空,xml根节点的所有的以“layout_”开头的属性都将失效,因为没有root对象来为根节点生成对应的LayoutParams对象。

针对该特性,如果传入的root为空,将出现类似如根节点定义的宽高失效,如我定义的根节点宽度为50dp,高度也为50dp,最后显示出来的效果却是一个wrap_content的效果。为什么会出现上述原因,是因为如果根节点没有LayoutParams对象,那么在它被add到某一个ViewGroup上的时候,将会自动生成一个宽高为wrap_content的LayoutParams对象:

ViewGroup#addView(android.view.View, int):

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // 如果LayoutParams为空的话,生成默认的
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

ViewGroup#generateDefaultLayoutParams:

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

总结

  1. LayoutInflater是android用来解析xml布局文件的一个类
  2. LayoutInflater内部使用Pull解析的方式,并对其进行了一定的扩展。
  3. LayoutInflater在生成View节点的时候,是通过反射的方式创建View对象,
    反射调用的构造方法是带两个参数的那个,所以在定义View的时候必须重写带两个参数的构造方法。
  4. LayoutInflater在创建View对象的时候,会将xml节点的解析器AttributeSet传入到View的构造方法中。AttributeSet定义了用来解析xml节点属性的API。View通过AttributeSet生成TypedArray,并从中读取View节点中定义的属性。
  5. 最后LayoutInflater将会通过递归的方式创建xml根节点下的所有孩子节点。
  6. LayoutInflater#inflate方法接收一个root对象以及一个Boolean类型的attachToRoot变量。这两个参数的值,直接影响了inflate方法的返回值,以及生成的xml根节点的LayoutParams和属性。
上一篇下一篇

猜你喜欢

热点阅读