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

作者: 良秋 | 来源:发表于2017-02-17 10:12 被阅读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和属性。

    相关文章

      网友评论

        本文标题:Android-LayoutInflater布局文件解析过程分析

        本文链接:https://www.haomeiwen.com/subject/rhukwttx.html