欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-14 15:07:40
android  layoutinflater加载布局详解 对于有一定android开发经验的同学来说,一定使用过layoutinflater.inflater...

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文件设定的属性值。需要在构造函数中将这些属性值取出来,赋给该实例的属性。

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

上一篇:

下一篇: