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

作者:Jaivne_Kuang 时间:2021-09-29 04:21:01 

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

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

来源:http://blog.csdn.net/javine/article/details/53284248

标签:Android,LayoutInflater,加载布局
0
投稿

猜你喜欢

  • SpringBoot使用SchedulingConfigurer实现多个定时任务多机器部署问题(推荐)

    2021-09-17 07:19:20
  • Java集合去重导致的线上问题

    2022-01-24 04:52:29
  • c#源码的执行过程详解

    2023-10-01 17:03:37
  • C#中Winform 实现Ajax效果自定义按钮

    2022-05-14 08:22:51
  • Java日常练习题,每天进步一点点(53)

    2023-08-12 00:55:00
  • Java 将PPT幻灯片转为HTML文件的实现思路

    2022-12-12 12:00:35
  • Java集合继承体系详解

    2023-12-03 00:22:13
  • java文件的重命名与移动操作实例代码

    2022-06-16 04:18:12
  • Java深入讲解Object类常用方法的使用

    2022-11-22 00:16:51
  • 浅谈byte和长度为8的boolean数组互相转换

    2023-11-07 00:34:37
  • SSM框架整合之junit测试的方法

    2022-11-12 16:03:19
  • C# Math中常用数学运算的示例详解

    2023-10-06 23:06:28
  • springmvc处理模型数据Map过程解析

    2022-04-24 01:06:01
  • Java裁剪压缩PNG图片,透明背景色变黑的解决方案

    2023-11-25 13:21:27
  • C#先判断是否存在再创建文件夹或文件与递归计算文件夹大小

    2023-07-29 00:04:58
  • Android开发实现抽屉菜单

    2022-09-01 11:47:09
  • java图形用户界面实现菜单功能

    2023-11-23 11:50:58
  • SSH框架网上商城项目第24战之Struts2中处理多个Model请求的方法

    2023-02-14 20:49:21
  • 在Android项目中使用AspectJ的方法

    2023-02-01 23:33:54
  • Java最全文件操作实例汇总

    2023-11-14 13:00:17
  • asp之家 软件编程 m.aspxhome.com