当前位置: 移动技术网 > IT编程>移动开发>Android > Android LayoutInflater加载布局详解及实例代码

Android LayoutInflater加载布局详解及实例代码

2019年07月24日  | 移动技术网IT编程  | 我要评论

总裁的六年宠物,风流老板俏秘书游戏,dy.9yh.org

android  layoutinflater加载布局详解

对于有一定android开发经验的同学来说,一定使用过layoutinflater.inflater()来加载布局文件,但并不一定去深究过它的原理,比如

1.layoutinflater为什么可以加载layout文件?
2.加载layout文件之后,又是怎么变成供我们使用的view的?
3.我们定义view的时候,如果需要在布局中使用,则必须实现带attributeset参数的构造方法,这又是为什么呢?

既然在这篇文章提出来,那说明这三个问题都是跟layoutinflater脱不了干系的。在我们的分析过程中,会对这些问题一一进行解答。

我们一步一步来,首先当我们需要从layout中加载view的时候,会调用这个方法

layoutinflater.from(context).inflater(r.layout.main_activity,null);

1.如何创建layoutinflater?

这有什么值得说的?如果你打开了layoutinflater.java你自然就明白了,layoutinflater是一个抽象类,而抽象类是不能直接被实例化的,也就是说我们创建的对象肯定是layoutinflater的某一个实现类。

我们进入layoutinflater.from方法中可以看到

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中获取,没吃过猪肉还没见过猪跑么,一说到context对象十有八九是说得contextimpl对象,于是我们直接去到contextimpl.java中,找到getsystemservice方法

@override
  public object getsystemservice(string name) {
    return systemserviceregistry.getsystemservice(this, name);
  }

额。。。又要去systemserviceregistry.java文件中

  /**
   * gets a system service from a given context.
   */
  public static object getsystemservice(contextimpl ctx, string name) {
    servicefetcher<?> fetcher = system_service_fetchers.get(name);
    return fetcher != null ? fetcher.getservice(ctx) : null;
  }

由代码可知,我们的service是从system_service_fetchers这个hashmap中获得的,而稍微看一下代码就会发现,这个hashmap是在static模块中赋值的,这里注册了很多的系统服务,什么activityservice,什么alarmservice等等都是在这个hashmap中。从layoutinflater.from方法中可以知道,我们找到是context.layout_inflater_service对应的service

registerservice(context.layout_inflater_service, layoutinflater.class,
        new cachedservicefetcher<layoutinflater>() {
      @override
      public layoutinflater createservice(contextimpl ctx) {
        return new phonelayoutinflater(ctx.getoutercontext());
      }});

好啦,主角终于登场了——phonelayoutinflater,我们获取的layoutinflater就是这个类的对象。

那么,这一部分的成果就是我们找到了phonelayoutinflater,具体有什么作用,后面再说。

2.inflater方法分析

这个才是最重要的方法,因为就是这个方法把我们的layout转换成了view对象。这个方法直接就在layoutinflater抽象类中定义

public view inflate(@layoutres int resource, @nullable viewgroup root) {
    return inflate(resource, root, root != null);
  }

传入的参数一个是layout的id,一个是是否指定parentview,而真正的实现我们还得往下看

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

我们先从context中获取了resources对象,然后通过res.getlayout(resource)方法获取一个xml文件解析器xmlresourceparser(关于在android中的xml文件解析器这里就不详细讲了,免得扯得太远,不了解的同学可以在网上查找相关资料阅读),而这其实是把我们定义layout的xml文件给加载进来了。

然后,继续调用了另一个inflate方法

public view inflate(xmlpullparser parser, @nullable viewgroup root, boolean attachtoroot) {
    synchronized (mconstructorargs) {
      final context inflatercontext = mcontext;
      //快看,view的构造函数中的attrs就是这个!!!
      final attributeset attrs = xml.asattributeset(parser);

      //这个数组很重要,从名字就可以看出来,这是构造函数要用到的参数
      mconstructorargs[0] = inflatercontext;
      view result = root;

      try {
        // 找到根节点,找到第一个start_tag就跳出while循环,
        // 比如<textview>是start_tag,而</textview>是end_tag
        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();

        //判断是否用了merge标签
        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 {
          // 这里需要调用到phonelayoutinflater中的方法,获取到根节点对应的view
          final view temp = createviewfromtag(root, name, inflatercontext, attrs);

          viewgroup.layoutparams params = null;
          //如果指定了parentview(root),则生成layoutparams,
          //并且在后面会将temp添加到root中
          if (root != null) {
            params = root.generatelayoutparams(attrs);
            if (!attachtoroot) {
              temp.setlayoutparams(params);
            }
          }

          // 上面解析了根节点,这里解析根节点下面的子节点
          rinflatechildren(parser, temp, attrs, true);

          if (root != null && attachtoroot) {
            root.addview(temp, params);
          }

          if (root == null || !attachtoroot) {
            result = temp;
          }
        }

      } catch (exception e) {

      } finally {
        // don't retain static reference on context.
        mconstructorargs[0] = lastcontext;
        mconstructorargs[1] = null;
      }

      return result;
    }
  }

这个就稍微有点长了,我稍微去除了一些跟逻辑无关的代码,并且添加了注释,如果有耐心看的话应该是能看懂了。这里主要讲两个部分,首先是rinflatechildren这个方法,其实就是一层一层的把所有节点取出来,然后通过createviewfromtag方法将其转换成view对象。所以重点是在如何转换成view对象的。

3.createviewfromtag

我们一层层跟进代码,最后会到这里

view createviewfromtag(view parent, string name, context context, attributeset attrs,
      boolean ignorethemeattr) {
      ......
      ......
    try {
      ......

      if (view == null) {
        final object lastcontext = mconstructorargs[0];
        mconstructorargs[0] = context;
        try {
          //不含“.” 说明是系统自带的控件
          if (-1 == name.indexof('.')) {
            view = oncreateview(parent, name, attrs);
          } else {
            view = createview(name, null, attrs);
          }
        } finally {
          mconstructorargs[0] = lastcontext;
        }
      }

      return view;
    } catch (inflateexception e) {
      throw e;
      ......
    }
  }

为了方便理解,将无关的代码去掉了,我们看到其实就是调用的createview方法来从xml节点转换成view的。如果name中不包含'.' 就调用oncreateview方法,否则直接调用createview方法。

在上面的phonelayoutinflater中就复写了oncreateview方法,而且不管是否重写,该方法最后都会调用createview。唯一的区别应该是系统的view的完整类名由oncreateview来提供,而如果是自定义控件在布局文件中本来就是用的完整类名。

4. createview方法

public final view createview(string name, string prefix, attributeset attrs)
      throws classnotfoundexception, inflateexception {
    //1.通过传入的类名,获取该类的构造器

    constructor<? extends view> constructor = sconstructormap.get(name);
    class<? extends view> clazz = null;

    try {
      if (constructor == null) {

        clazz = mcontext.getclassloader().loadclass(
            prefix != null ? (prefix + name) : name).assubclass(view.class);

        if (mfilter != null && clazz != null) {
          boolean allowed = mfilter.onloadclass(clazz);
          if (!allowed) {
            failnotallowed(name, prefix, attrs);
          }
        }
        constructor = clazz.getconstructor(mconstructorsignature);
        constructor.setaccessible(true);
        sconstructormap.put(name, constructor);
      } else {

        if (mfilter != null) {
          boolean allowedstate = mfiltermap.get(name);
          if (allowedstate == null) {     
            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);
          }
        }
      }

      //2.通过获得的构造器,创建view实例
      object[] args = mconstructorargs;
      args[1] = attrs;

      final view view = constructor.newinstance(args);
      if (view instanceof viewstub) {
        final viewstub viewstub = (viewstub) view;
        viewstub.setlayoutinflater(cloneincontext((context) args[0]));
      }
      return view;

    } catch (nosuchmethodexception e) {
     ......
    } 
  }

这段代码主要做了两件事情

第一,根据classname将类加载到内存,然后获取指定的构造器constructor。构造器是通过传入参数类型和数量来指定,这里传入的是mconstructorsignature

static final class<?>[] mconstructorsignature = new class[] {
      context.class, attributeset.class};

即传入参数是context和attributeset,是不是猛然醒悟了!!!这就是为什么我们在自定义view的时候,必须要重写view(context context, attributeset attrs)则个构造方法,才能在layout中使用我们的view。

第二,使用获得的构造器constructor来创建一个view实例。

5.回答问题

还记得上面我们提到的三个问题吗?现在我们来一一解答:

1.layoutinflater为什么可以加载layout文件?

因为layoutinflater其实是通过xml解析器来加载xml文件,而layout文件的格式就是xml,所以可以读取。

2.加载layout文件之后,又是怎么变成供我们使用的view的?

layoutinflater加载到xml文件中内容之后,通过反射将每一个标签的名字取出来,并生成对应的类名,然后通过反射获得该类的构造器函数,参数为context和attributeset。然后通过构造器创建view对象。

3.我们定义view的时候,如果需要在布局中使用,则必须实现带attributeset参数的构造方法,这又是为什么呢?

因为layoutinflater在解析xml文件的时候,会将xml中的内容转换成一个attributeset对象,该对象中包含了在xml文件设定的属性值。需要在构造函数中将这些属性值取出来,赋给该实例的属性。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网