在我们的日常开发中,LayoutInflater非常常见。它可以将布局文件加载成一个View实例。那么,LayoutInflater是如何将一个布局文件加载成View实例的呢?
一、LayoutInflater的获取
LayoutInflater的获取有三种方式:
1. LayoutInflater.from(context)。
2. context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)。
3. 在Activity中还可以通过getLayoutInflater()方法获取。
第一种方式和第二种本质上其实是同一种。只是系统给我们封装了一下方便我们调用。
```java
/** * Obtains the LayoutInflater from the given context. */ public static LayoutInflater from(Context context) { LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); //。。。 return LayoutInflater; }
```
对于第三种Activity#getLayoutInflater()方法:
```java
public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); }
```
可以看到它其实调用了Window的getLayoutInflater方法,Window的唯一实现类是PhoneWindow,点进去看看。
```java
@Override public LayoutInflater getLayoutInflater() { return mLayoutInflater; }
```
emmmm直接返回了mLayoutInflater对象,那么mLayoutInflater在哪里初始化的呢?找找...
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
可以看到在PhoneWindow构造方法中初始化mLayoutInflater,原来还是通过LayoutInflater.from(context)获取的啊!所以不管通过哪种方式最终都是通过context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取的。
二、LayoutInflater的inflate方法
为了更好的理解源码,先来理解inflate方法中下面这几个入参的含义:
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
这里写个demo做测试,demo的Activity根布局就是一个LinearLayout,其id为ll_content。然后在创建一个布局文件layout_inflate_test.xml如下:
```xml
android:layout_width="match_parent" android:layout_height="80dp" android:background="@android:color/darker_gray" android:text="Hello World!" /> ``` 然后在代码中通过LayoutInflater的inflate方法将layout_inflate_test.xml这个布局文件加载到id为ll_content的LinearLayout中。 在Android开发中,`LayoutInflater` 是一个非常常用的工具类,它用于将XML布局文件转换为对应的View对象。我们可以通过调用 `inflate()` 方法来实现这个功能。下面我们将通过三种方式来演示如何使用 `LayoutInflater`。 首先,我们来看第一种方式: ```java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_layout_inflate_demo); LinearLayout ll_content = findViewById(R.id.ll_content); LayoutInflater inflater = LayoutInflater.from(this); // 方式1 inflater.inflate(R.layout.layout_inflate_test, ll_content); } ``` 在这个例子中,我们首先获取了一个 `LinearLayout` 对象 `ll_content`,然后通过 `LayoutInflater.from(this)` 获取了一个 `LayoutInflater` 实例。接着,我们调用了 `inflater.inflate()` 方法,将 `layout_inflate_test.xml` 布局文件加载到 `ll_content` 中。 接下来,我们来看第二种方式: ```java // 方式2 TextView textView = (TextView) inflater.inflate(R.layout.layout_inflate_test, ll_content, false); ll_content.addView(textView); ``` 在这个例子中,我们同样获取了一个 `LinearLayout` 对象 `ll_content`,然后通过 `LayoutInflater.from(this)` 获取了一个 `LayoutInflater` 实例。接着,我们调用了 `inflater.inflate()` 方法,将 `layout_inflate_test.xml` 布局文件加载到一个新的 `TextView` 对象中。注意,我们在调用 `inflate()` 方法时传入了第三个参数 `false`,这意味着加载后的布局不会自动添加到 `ll_content` 中,而是返回一个已经加载好的 `View` 实例。最后,我们需要手动调用 `addView()` 方法将其添加到 `ll_content` 中。 最后,我们来看第三种方式: ```java // 方式3 TextView textView = (TextView) inflater.inflate(R.layout.layout_inflate_test, null); ll_content.addView(textView); ``` 在这个例子中,我们同样获取了一个 `LinearLayout` 对象 `ll_content`,然后通过 `LayoutInflater.from(this)` 获取了一个 `LayoutInflater` 实例。接着,我们调用了 `inflater.inflate()` 方法,将 `layout_inflate_test.xml` 布局文件加载到一个新的 `TextView` 对象中。但是这次,我们在调用 `inflate()` 方法时传入了第二个参数 `null`,这意味着加载后的布局不会自动添加到任何容器中,而是直接返回一个已经加载好的 `View` 实例。因此,我们需要手动调用 `addView()` 方法将其添加到 `ll_content` 中。 以下是重构后的内容: public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } 当调用inflate的两个参数的重载方法时,内部会调用三参的。如果第二个参数root不为null,那么调用三参数的inflate方法时,第三个参数就是true。 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); //XmlResourceParser用来解析XML final XmlResourceParser parser = res.getLayout(resource); if (root != null) { //通过parser解析传入的布局文件,使用的是Pull解析方式 //..... } //将解析后的布局文件传入到inflate方法中 return inflate(parser, root, attachToRoot); public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) throws InflateException { synchronized (mConstructorArgs) { View result = root; try { //查找布局文件的根节点 while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } //获取到布局文件的根节点的名称 final String name = parser.getName(); //处理根节点是merge标签的情况 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { //从异常可以看出,如果被加载布局的根节点是merge,那么必须满足root !=null & attachToRoot==true。 throw new InflateException(" "ViewGroup root and attachToRoot=true"); } //调用rInflate递归加载,注意这里传入的第二个参数是root。等会看 rInflate(parser, root, inflaterContext, attrs, false); } else { //创建name标签对应的View。(现在是根标签) final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { //重点:如果传入的root不为空,会通过root的generateLayoutParams为当前布局生成LayoutParams params = root.generateLayoutParams(attrs); if (!attachToRoot) { //如果attachToRoot为false就会为生成的temp这个View设置tLayoutParams temp.setLayoutParams(params); } } //加载完根View后就会去递归加载它下面所有的孩子。 rInflateChildren(parser, temp, attrs, true); //如果root != null & attachToRoo==true就调用root.addView方法将temp这个View添加进去,并且传入生成的布局参数。 if (root != null && attachToRoot) { root.addView(temp, params); } else if (root == null || !attachToRoot) { // 如果root == null || attachToRoot==false 就将temp赋值给result然后返回。 result = temp; } } return result; } catch (Exception e) { // Handle exceptions here before rethrowing. throw new InflateException("Error inflating view", e); } finally { parser.reset(); // Reset the parser to prepare it for parsing the next view. } } } 通过这段源码,我们可以了解到以下几点: 1. 如果`root`不为空,那么会借助`root`的`generateLayoutParams`方法为生成的`view`获取其`LayoutParams`,包含了`layout_width`和`layout_height`。 2. 如果`root`不为空且`attachToRoot`为`false`,则会通过`root.generateLayoutParams(attrs)`为这个`view`生成`LayoutParams`,并将其设置到`view`中。最后返回这个`view`而不是`root`。 3. 如果`root`不为空且`attachToRoot`为`true`,则会通过`root.generateLayoutParams(attrs)`为这个`view`生成`LayoutParams`,并调用`root.addView(temp, params)`方法将`temp`加入到`root`中,同时添加的还有`params`。 4. 如果`root`为空,则不会为生成的`View`生成`LayoutParams`,最后返回的是这个`view`,而不是`root`。 通过以上分析,我们应该已经理解了上面示例中的`TextView`高度未生效的原因。原来是因为`root`为空,所以没有为这个视图设置`LayoutParams`,因此我们在调用自己的`addView`方法时,采用的是默认的`LayoutParams`。 接下来让我们继续研究源码。注意到这行代码: ```java final View temp = createViewFromTag(root, name, inflaterContext, attrs); ``` 根据节点的名称创建对应的视图。例如,如果我们传入的标签名称是`TextView`,那么就会根据这个名称生成一个`TextView`实例。 ```java View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { // 如果标签名称是blink 就生成一个BlinkLayout布局,blink 标签很少使用,它可以达到一闪一闪的效果,不重点看了。 if (name.equals(TAG_1995)) { return new BlinkLayout(context, attrs); } View view; if (mFactory2 != null) { // 如果mFactory2 不为空就调用它的onCreateView来创建View。 view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { // 如果mFactory不为空就调用它的onCreateView来创建View。 view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory != null) { // 如果上面没有创建View 并且mPrivateFactory不为空就调用它的onCreateView来创建View。 view = mPrivateFactory.onCreateView(parent, name, context, attrs); } if (view == null) { // 如果经过上面的步骤view没有创建,就调用它自己的方法来创建view try { if (-1 == name.indexOf('.')) { // 如果标签名中不包含.比如TextView,ImageView就调用onCreateView方法 view = onCreateView(parent, name, attrs); } else { // 如果包含.比如support库中的View或者我们自定义的View都是全路径xx.xxx.xxx。就调用createView方法。 view = createView(name, null, attrs); } } catch (Exception e) { e.printStackTrace(); } } return view; } ``` 从源码中我们可以看出,createViewFromTag方法首先尝试通过mFactory2和mFactory的onCreateView方法来创建View实例。这里,Factory其实是系统预留给我们的钩子,我们可以通过setFactory和setFactory2方法来设置mFactory和mFactory2,以拦截LayoutInflate自带的createView方式。这样,常见的换肤、统一字体大小等需求都可以通过这种方式实现。 LayoutInflate在创建View时分为两种情况调用:onCreateView和createView。如果标签名不包含".",说明这是一个系统自带的View,例如TextView、ImageView等,此时会调用onCreateView方法。而对于support库中的View或者我们自定义的View(包含"."),则会调用createView方法。 LayoutInflater是一个抽象类,它的实现类是PhoneLayoutInflater。PhoneLayoutInflater实现了onCreateView方法,我们可以查看这个方法的具体实现来了解它是如何工作的。 public class PhoneLayoutInflater extends LayoutInflater { private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack at it. break; } } return super.onCreateView(name, attrs); } private View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException { // 实现具体的创建View实例的逻辑,根据prefix拼接类名,然后调用loadClass方法加载类并创建实例 // 这里需要根据具体需求来实现 throw new UnsupportedOperationException("Not implemented yet"); } } protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { // 尝试使用前缀 "android.view" 再次创建视图 return createView(name, "android.view.", attrs); } 最终都会走到createView方法,点进去看看: ```java public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { //1、先尝试从缓存中获取 Constructor extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class extends View> clazz = null; try { //2、缓存中获取不到 if (constructor == null) { //通过前缀和标签名拼接获取全路径,然后获取到该类的class clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); //通过class获取到构造 constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); //保存到缓存中 sConstructorMap.put(name, constructor); } else { //。。。 } //通过反射创建View实例 final View view = constructor.newInstance(args); //。。。 return view; } catch (NoSuchMethodException e) { throw new ClassNotFoundException("找不到对应的构造方法", e); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new InflateException("创建View实例失败", e); } finally { if (constructor != null) { constructor.setAccessible(false); } } } ``` 感谢您的分享。根据您提供的内容,createViewFromTag方法是通过节点的name实例化出一个具体的View,而inflate中的另一个方法是大致回顾下inflate方法。如果您还有其他问题或者需要帮助的话,请随时告诉我哦。 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } final String name = parser.getName(); if (TAG_MERGE.equals(name)) { // TODO: Add your implementation for handling TAG_MERGE here } else { // Create a temporary View based on the layout root node's name and attributes final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { params = root.generateLayoutParams(attrs); if (!attachToRoot) { temp.setLayoutParams(params); } } // Load children from the root node below, passing in the temporary view as the current node's view. rInflateChildren(parser, temp, attrs, true); if (root != null && attachToRoot) { // Add the temporary view, which represents the layout root node, to the passed-in root root.addView(temp, params); } if (root == null || !attachToRoot) { result = temp; } } return result; } } InflateChildren方法是一个递归调用的方法,它的作用是解析parent下的子节点。在这个方法中,它首先调用了rInflate方法来解析子节点。 以下是重构后的代码: ```java final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { rInflate(parser, parent, parent.getContext(), attrs, finishInflate); } private void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { // 解析parent下的子节点的逻辑 } ``` 在这段代码中,我们将rInflateChildren方法中的rInflate方法提取出来,并将其定义为一个私有方法。这样可以使代码更加清晰和易于维护。同时,我们也保留了原始的rInflateChildren方法,以便在需要时进行递归调用。 ```java void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { // 解析并处理不同类型的标签 int type; int depth = 1; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } // 获取节点的名称 final String name = parser.getName(); if (TAG_REQUEST_FOCUS.equals(name)) { // 处理节点名称是requestFocus的情况 pendingRequestFocus = true; consumeChildElements(parser); } else if (TAG_TAG.equals(name)) { // 处理节点是tag的情况 parseViewTag(parser, parent, attrs); } else if (TAG_INCLUDE.equals(name)) { // 处理节点是include的情况 if (parser.getDepth() == 0) { throw new InflateException(" } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { // 如果是merge标签就抛异常,可以看出merge标签只能作为布局文件的根节点。 throw new InflateException(" } else { // 通过createViewFromTag方法创建当前节点的View实例 final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); // 继续递归加载当前节点下的子View rInflateChildren(parser, view, attrs, true); // 加载完成后将当前节点添加到parent中(PS:此时当前节点下的子View已经add到当前节点了) viewGroup.addView(view, params); } } // 如果pendingRequestFocus为true,恢复parent的默认焦点 if (pendingRequestFocus) { parent.restoreDefaultFocus(); } // 如果finishInflaiewte为true,调用onFinishInflate方法(又看到一个熟悉的方法) if (finishInflate) { parent.onFinishInflate(); } } ``` 整个加载过程是一个递归的过程,其主要步骤如下: 1. 首先,rInflate函数被调用来解析传入的布局参数。此函数接收一个parent参数,这个参数表示当前正在创建的视图的父视图。 2. rInflate函数会将布局文件中定义的所有子节点都创建出来,并将它们添加到parent视图中。在这个过程中,如果遇到有其他子节点的节点(例如嵌套的ListView或者GridView),rInflate函数会递归地调用自己,为这些子节点也创建对应的视图。 3. 在所有的子节点都被成功添加到parent视图后,rInflate函数返回。此时已经成功地解析了根节点,并且所有在其下的子节点也被成功创建并添加到了父视图中。 接下来是当根节点是merge的情况。在最开始加载布局文件的根节点时,如果根节点是一个merge标签,那么就需要进行一些特殊的处理。 对于这种情况,Android系统会在布局文件中查找是否有同名的view已经存在。如果找到了同名的view,那么就会使用找到的那个view作为新的merge标签对应的view。这就是所谓的"合并"或者"替换"操作。 这样,通过以上的过程,我们就可以成功地从XML布局文件中加载出所有的视图,并且将它们按照正确的顺序和层级组织在一起,形成一个完整的View树。 以下是重构后的代码: public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); // 查找布局文件的根节点 while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } //获取节点的名称 final String name = parser.getName(); if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException(" "ViewGroup root and attachToRoot=true"); } //可以看到这里调用了rInflate方法 并且parent传入的参数的root rInflate(parser, root, inflaterContext, attrs, false); } } return result; } 在处理merge标签时,会调用`rInflate`方法递归加载所有子View。最终这些子View会被添加到根节点中,这时我们就明白了`merge`标签是如何减少布局嵌套的。因为在使用`merge`标签时,它的直接子节点在未被创建视图时就被直接添加到了根节点。同时,`rInflate`方法的最后一个参数为`false`,这意味着在直接子View都添加到根节点后,不会调用根节点的`onFinishInflate`方法。 如果需要执行`onFinishInflate`,可以调用`parent.onFinishInflate();`方法。需要注意的是,这里的`root`与根节点并不是同一个概念。`root`指的是调用`LayoutInflater`的`inflate`方法时传入的参数。