浅谈 LayoutInflater

作者: 牙锅子 | 来源:发表于2016-10-23 10:55 被阅读144次

    版权声明:本文为博主原创文章,未经博主允许不得转载。
    微博:厉圣杰
    源码:AndroidDemo/View
    文中如有纰漏,欢迎大家留言指出。

    在 Android 的开发中,想必大家都用过 LayoutInflater 吧。恩,就是平时自定义控件经常会用到的。啊,你连自定义控件都没有用到过?不要紧,那 Activity 中的 setContentView() 肯定用到过吧。通过查看 Android 源码,你会发现,其实 setContentView() 方法最终也是调用 LayoutInflater 类。说到这,大家想必应该能猜到 LayoutInflater 这个类是干嘛的了吧?

    LayoutInflater 类就是用来加载布局的,它的用法很简单,我们可以通过两种方式来获取 LayoutInflater 的对象。第一种方式是我们平时自定义 View 的时候会经常使用的,写法如下:

    LayoutInflater inflater = LayoutInflater.from(context);
    

    第二种方式就是调用系统服务,写法如下:

    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.Layout_INFLATER_SERVICE);
    

    通过查看 LayoutInflater.from(Context context) 方法的源码,你会发现其实它还是通过调用系统服务来获取 LayoutInflater 对象。

    /**
    * Obtains the LayoutInflater from the given 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;
    }
    

    获取 LayoutInflater 对象之后,我们就可以通过调用 inflate() 方法来加载布局,inflate() 有四种重载形式,源码如下,先重点看方法名和参数就好,后面还会回过来讲:

    //方法一
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        //当root==null时,attachToRoot=false
        return inflate(resource, root, root != null);
    }
    
    //方法二
    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();
       }
    }
    
    //方法三
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
            return inflate(parser, root, root != null);
    }
    
    //方法四
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
       synchronized (mConstructorArgs) {
           Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
    
           final Context inflaterContext = mContext;
           final AttributeSet attrs = Xml.asAttributeSet(parser);
           Context lastContext = (Context) mConstructorArgs[0];
           mConstructorArgs[0] = inflaterContext;
           View result = root;
    
           try {
               // Look for the root node.
               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 (DEBUG) {
                   System.out.println("**************************");
                   System.out.println("Creating root view: "
                           + name);
                   System.out.println("**************************");
               }
    
               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 {
                   // Temp is the root view that was found in the xml
                   final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
                   ViewGroup.LayoutParams params = null;
    
                   if (root != null) {
                       if (DEBUG) {
                           System.out.println("Creating params from root: " +
                                   root);
                       }
                       // Create layout params that match root, if supplied
                       params = root.generateLayoutParams(attrs);
                       if (!attachToRoot) {
                           // Set the layout params for temp if we are not
                           // attaching. (If we are, we use addView, below)
                           temp.setLayoutParams(params);
                       }
                   }
    
                   if (DEBUG) {
                       System.out.println("-----> start inflating children");
                   }
    
                   // Inflate all children under temp against its context.
                   rInflateChildren(parser, temp, attrs, true);
    
                   if (DEBUG) {
                       System.out.println("-----> done inflating children");
                   }
    
                   // We are supposed to attach all the views we found (int temp)
                   // to root. Do that now.
                   if (root != null && attachToRoot) {
                       root.addView(temp, params);
                   }
    
                   // Decide whether to return the root that was passed in or the
                   // top view found in xml.
                   if (root == null || !attachToRoot) {
                       result = temp;
                   }
               }
    
           } catch (XmlPullParserException e) {
               final InflateException ie = new InflateException(e.getMessage(), e);
               ie.setStackTrace(EMPTY_STACK_TRACE);
               throw ie;
           } catch (Exception e) {
               final InflateException ie = new InflateException(parser.getPositionDescription()
                       + ": " + e.getMessage(), e);
               ie.setStackTrace(EMPTY_STACK_TRACE);
               throw ie;
           } finally {
               // Don't retain static reference on context.
               mConstructorArgs[0] = lastContext;
               mConstructorArgs[1] = null;
    
               Trace.traceEnd(Trace.TRACE_TAG_VIEW);
           }
    
           return result;
       }
    }
    

    通常,我们使用前两种方式来加载布局,也就是靠传递布局的 ID 来实现加载,而不是使用后两种方式通过 XmlPullParser 来实现布局加载。而通过查看上述源码,不难发现前面三种方法最终都是调用第四种方式来加载布局,所有布局加载的主要逻辑也集中在第四个方法中。通过第四个方法的参数,我们可以知道 LayoutInflater 是通过 Pull 方式来解析布局文件的

    通过以上简单的阐述,我们来分析一下 inflate() 前两种方法的使用。

    有时候,学以致用这是一个让人纠结的命题,明明是先学后用,但有时候先用后学会比先学后用起到更好的效果。我想这也是现在某些速成培训班流行的结果,这里并不是讽刺,有时候实践的确比原理重要,就像很多理论都是先发现现象再去探究原因。

    下面,我们就通过几个简单的例子来透过现象看本质。

    首先,我们来创建需要被 LayoutInflater 载入的布局 layout_single_button.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <Button xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="我是一个按钮">
    
    </Button>
    

    这真是一个简单的不能再简单的布局,只有一个 Button 控件。

    MainActivity 的 布局文件 activity_main.xml 如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_layout_inflater"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.littlejie.view.LayoutInflaterActivity">
    
        <Button
            android:id="@+id/btn_root_is_not_null"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="root!=null" />
    
        <Button
            android:id="@+id/btn_root_is_null"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:width="100dp"
            android:height="100dp"
            android:text="root=null" />
    
        <!-- 容器,用于直观的在Activity上动态演示添加操作-->
        <LinearLayout
            android:id="@+id/ll_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"></LinearLayout>
    
    </LinearLayout>
    

    现在我们用 LayoutInflater 将 layout_single_button.xml 中的按钮添加到 activity_main.xml 中的 ll_container 中, MainActivity 的代码如下:

    public class LayoutInflaterActivity extends Activity implements View.OnClickListener {
    
        private static final String TAG = LayoutInflaterActivity.class.getSimpleName();
    
        private LinearLayout mLlContainer;
        private LayoutInflater mInflater;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_layout_inflater);
    
            findViewById(R.id.btn_root_is_not_null).setOnClickListener(this);
            findViewById(R.id.btn_root_is_null).setOnClickListener(this);
    
            mLlContainer = (LinearLayout) findViewById(R.id.ll_container);
            //获取LayoutInflater实例
            //也可以通过 mInflater = (LayoutInflater) this.getSystemService(Context.Layout_INFLATER_SERVICE)来获取
            mInflater = LayoutInflater.from(this);
        }
    
        @Override
        public void onClick(View v) {
            //执行点击事件前,先把mLlContainer中的view移除
            mLlContainer.removeAllViews();
            switch (v.getId()) {
                case R.id.btn_root_is_not_null:
                    inflateWithRoot();
                    break;
                case R.id.btn_root_is_null:
                    inflateWithoutRoot();
                    break;
            }
        }
    
        /**
         * 使用 inflate(int resource,ViewGroup root) 载入布局
         * root != null
         */
        private void inflateWithRoot() {
            View inflateView = mInflater.inflate(R.layout.layout_single_button, mLlContainer);
            Log.d(TAG, "method = inflateWithRoot,inflaterView type = " + inflateView.getClass().getSimpleName());
            //此处必须注释掉,否则会抛出如下crash
            //java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first
            //mLlContainer.addView(inflateView);
        }
    
        /**
         * 使用 inflate(int resource,ViewGroup root) 载入布局
         * root != null
         */
        private void inflateWithoutRoot() {
            View inflateView = mInflater.inflate(R.layout.layout_single_button, null);
            Log.d(TAG, "method = inflateWithoutRoot,inflaterView type = " + inflateView.getClass().getSimpleName());
            //此处注释必须要去掉,否则看不到载入的button
            //mLlContainer.addView(inflateView);
        }
    }
    

    运行程序,分别点击两个按钮,输出日志如下:

    10-22 21:08:57.680 28603-28603/com.littlejie.view D/LayoutInflaterActivity: method = inflateWithRoot,inflaterView type = LinearLayout
    10-22 21:08:58.990 28603-28603/com.littlejie.view D/LayoutInflaterActivity: method = inflateWithoutRoot,inflaterView type = Button
    

    你会发现执行 inflateWithRoot() 方法会在屏幕上显示 Button,而执行 inflateWithoutRoot() 方法并没有显示 Button。

    现在我们去掉注释,再重新执行一遍,咦,执行 inflateWithRoot() 方法竟然崩溃了,而执行 inflateWithoutRoot() 方法的却在屏幕上显示 Button。现象在注释里都写,那你知道为什么嘛?还有,执行点击事件的日志,View Type 为什么一个是 Button,而另一个确实 LinearLayout,我们明明载入的是 Button 啊~

    带着这些疑问,我们去看 Android 源码,前面有提到, inflate() 方法的最终都是调用第四种重载形式来实现载入布局,所以我们重点要看方法四的代码。你可能已经注意到方法四有一个 attachToRoot 参数,那么方法一是怎么获取这个参数的呢?查看源码,可以很容易的知道:

    attachToRoot = root != null
    

    关于 root 和 attachToRoot 的组合效果会在后面给出结论。

    我们接着来看源码:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
       synchronized (mConstructorArgs) {
           //先把root赋给result
           View result = root;
    
           try {
               //Todo:解析XML
    
               final String name = parser.getName();
               
               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 {
                   // Temp is the root view that was found in the xml
                   final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                   
                   //Todo
                   
                   // Inflate all children under temp against its context.
                   rInflateChildren(parser, temp, attrs, true);
    
                   // We are supposed to attach all the views we found (int temp)
                   // to root. Do that now.
                   if (root != null && attachToRoot) {
                       root.addView(temp, params);
                   }
    
                   // Decide whether to return the root that was passed in or the
                   // top view found in xml.
                   if (root == null || !attachToRoot) {
                       result = temp;
                   }
               }
    
           } catch () {
               //Todo
           } finally {
               //Todo
           }
           return result;
       }
    }
    

    可以发现 LayoutInflater 调用 createViewFromTag() 方法来创建布局的 Root View ,然后通过调用 rInflateChildren() 方法来循环遍历生成 子View 。(rInflateChildren() 最终是调用 rInflate() 方法来生成 View,有兴趣的可以去看下源码)接下去的两个 if语句 是我们此次分析的重点

    对于 inflate(int resouce,ViewGroup root) 来说,两个 if语句 是互斥的。那么上述问题就很好解释了:

    • inflateWithRoot() 方法因为 root != null 导致只会执行第一个 if语句,因此会将布局layout_single_button.xml 中的 Button 添加到 mLlContainer 中,这也就解释了为什么返回的 View 是 LinearLayout 类型,而执行 mLlContainer.addView() 会抛出 java.lang.IllegalStateException
    • inflateWithoutRoot() 方法因为 root == null 导致只执行第二个 if语句,所以返回的 View 是 Button 类型,并且没有 父View ,所以如果不执行 mLlContainer.addView() ,就不会显示在屏幕上。

    看到这里,相比你已经知道 root 和 attachToRoot 的作用了吧。原谅我偷懒,以下内容摘自郭霖博客

    1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
    2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。
    3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
    4. 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

    补充

    看见补充部分,想必你肯定认真的看完了前面的内容,这里咱们来谈谈一些刚刚没细谈的。

    • 开头说到的关于 setContentView() 其实也是调用 LayoutInflater 类来实现布局加载的事情,可能你通过 Android Studio 查看源码,发现最终调用的 Window 类 setContentView()方法:
    /**
     * Abstract base class for a top-level window look and behavior policy.  An
     * instance of this class should be used as the top-level view added to the
     * window manager. It provides standard UI policies such as a background, title
     * area, default key processing, etc.
     *
     * <p>The only existing implementation of this abstract class is
     * android.view.PhoneWindow, which you should instantiate when needing a
     * Window.
     */
    public abstract class Window {
        //...
        public abstract void setContentView(@LayoutRes int layoutResID);
        //...
    }
    

    咦,竟然是抽象类的一个抽象方法,恩,那肯定是在子类中实现的。赶紧看下有哪个类是继承 Window 的。用 Android Studio 一看,我勒个去,这货没子类?这是什么鬼?
    ![屏幕快照 2016-10-22 下午9.41.43](http://odsdowehg.bkt.clouddn.com/屏幕快照 2016-10-22 下午9.41.43.png)

    等等,再看一眼类注释,好像有说 Window 有个实现类 android.view.PhoneWindow (位于包:package com.android.internal.policy;)。通常,你可以在以下目录中找到 PhoneWindow 类:

    android-sdk-path/sources/android-24/com/android/internal/policy
    

    如果你下载 AOSP 源码,你可以在以下目录找到 PhoneWindow :

    android-source-code/frameworks/base/core/java/com/android/internal/policy
    

    setContentView() 方法源码摘录如下:

    @Override
    public void setContentView(int layoutResID) {
       // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
       // decor, when theme attributes and the like are crystalized. Do not check the feature
       // before this happens.
       if (mContentParent == null) {
           installDecor();
       } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           mContentParent.removeAllViews();
       }
    
       if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                   getContext());
           transitionTo(newScene);
       } else {
           mLayoutInflater.inflate(layoutResID, mContentParent);
       }
       mContentParent.requestApplyInsets();
       final Callback cb = getCallback();
       if (cb != null && !isDestroyed()) {
           cb.onContentChanged();
       }
       mContentParentExplicitlySet = true;
    }
    

    可以看见一行代码 mLayoutInflater.inflate(layoutResID, mContentParent);,所以 setContentView() 方法也是通过 LayoutInflater 来实现布局加载,只不过 Android 为了方便我们使用对此进行了封装而已。

    • 你可能注意到 inflate() 方法中有这么一个判断:
    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);
     }
    

    所以在使用 <merge> 标签的时候要一定要设置 root,否则会抛出异常。

    总结

    尝试自己阅读源码解释清楚某个问题现象,不过逻辑还是有点不清晰,语言组织的也不是很好,再接再厉~

    不积跬步无以至千里
    不积小流无以成江海

    相关文章

      网友评论

        本文标题:浅谈 LayoutInflater

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