美文网首页自定义view相关Android 日记Android开发
探索Android视图加载器LayoutInflater

探索Android视图加载器LayoutInflater

作者: 沈敏杰 | 来源:发表于2016-11-05 00:07 被阅读1682次

    这次跟大家分享的是关于LayoutInflater的使用,在开发的过程中,LayoutInfalter经常用于加载视图,对,今天咱们来聊的就是,关于加载视图的一些事儿,我记得之前一位曾共事过的一位同事问到我一个问题,activity是如何加载资源文件来显示界面的,古话说得好,知其然不知其所以然,因此在写这篇文章的时候我也做了不少的准备,在这里我先引出几个问题,然后我们通过问题在源码中寻找答案。

    1.如何获取LayoutInflater?
    2.如何使用LayoutInflater?为什么?
    3.Activity是如何加载视图的?
    4.如何优化我们的布局?

    首先我们先看一下LayoutInflater是如何获取的。

    LayoutInflater inflater=LayoutInflater.from(context);
    

    我们通过LayoutInflater.from(Context)获取LayoutInflater,我们继续进入LayoutInflater.java探索一番。
    LayoutInflater.java:

    /**
     * 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里面,通过静态方法from(Context),然后继续调用Context中的方法getSystemService获取LayoutInflater,我们往Context继续看。

    Context.java:

    public abstract Object getSystemService(@ServiceName @NonNull String name);
    
    

    大家会发现,怎么点进去这是个抽象方法,其实Context是一个抽象类,真正实现的是ContextImpl这个类,我们就继续看ContextImpl:
    ContextImpl.java:

    @Override
    public Object getSystemService(String name) {
        //继续调用SystemServiceRegistry.getSystemService
        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获取LayoutInflate
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
    

    SystemServiceRegistry这个类中的静态方法getSystemService,通过SYSTEM_SERVICE_FETCHERS获取ServiceFetcher,我们先看看SYSTEM_SERVICE_FETCHERS跟ServiceFetcher在SystemServiceRegistry中的定义。
    SystemServiceRegistry.java:

    //使用键值对来保存
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();
    
    /**
     * Base interface for classes that fetch services.
     * These objects must only be created during static initialization.
     */
    static abstract interface ServiceFetcher<T> {
        //只有一条接口,通过context获取服务,先看一下其实现类
        T getService(ContextImpl ctx);
    }
    

    SYSTEM_SERVICE_FETCHERS在SystemServiceRegistry这个类中作为全局常量,通过键值对的方式用来保存ServiceFetcher,而ServiceFetcher又是什么?在源码中,ServiceFetcher是一条接口,通过泛型T定义了getService(ContextImpl)来获取服务对象。那么具体ServiceFetcher具体的实现在什么地方?在SystemServiceRegistry中,有一段这样的代码:
    SystemServiceRegistry.java:

    static {
        .....
        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
            new CachedServiceFetcher<LayoutInflater>() {
        @Override
        public LayoutInflater createService(ContextImpl ctx) {
            return new PhoneLayoutInflater(ctx.getOuterContext());
        }});
    
    ......
    }
    

    在这个静态代码块中,通过registerService进行初始化注册服务。我们先看看这个静态方法。

    /**
     * Statically registers a system service with the context.
     * This method must be called during static initialization only.
     */
    private static <T> void   registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }
    
    

    registerService这个一段函数的作用就是用来通过键值对的方式,保存服务对象,也就是说,SystemServiceRegistry会初始化的时候注册各种服务,而我们的也看到Context.LAYOUT_INFLATER_SERVICE作为key来获取LayoutInfalter。

    Context.java:

    /**
     * 定义这个常量,用于获取系统服务中的LayoutInflate
     * Use with {@link #getSystemService} to retrieve a
     * {@link android.view.LayoutInflater} for inflating layout resources in this
     * context.
     *
     * @see #getSystemService
     * @see android.view.LayoutInflater
     */
    public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
    

    我们继续看看ServiceFetcher的实现类:

    /**
     * Override this class when the system service constructor needs a
     * ContextImpl and should be cached and retained by that context.
     */
    static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
        private final int mCacheIndex;
    
        public CachedServiceFetcher() {
            mCacheIndex = sServiceCacheSize++;
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public final T getService(ContextImpl ctx) {
            final Object[] cache = ctx.mServiceCache;
            synchronized (cache) {
                // Fetch or create the service.
                Object service = cache[mCacheIndex];
                if (service == null) {
                    service = createService(ctx);
                    cache[mCacheIndex] = service;
                }
                return (T)service;
            }
        }
    
        public abstract T createService(ContextImpl ctx);
    }
    

    CachedServiceFetcher的作用用于保存我们的泛型T,同时这个CachedServiceFetcher有一个抽象方法createService,createService这个方法用来创建这个服务,因此使用这个类就必须重写这个方法,我们继续看回:

    ServiceFetcher.java:

    staic{
        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
        }});
    }
    

    现在看回来这里,注册服务不就是通过通过键值对的方式进行保存这个对象,然而我们获取到的LayoutInflater其实是PhoneLayoutInflater。PhoneLayoutInflater继承于LayoutInfalter.

    小结:

    我们获取LayoutInflater对象,可以通过两种方法获取:

    LayoutInflater inflater1=LayoutInflater.from(context);
    LayoutInflater inflater2= (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    

    context的实现类contextImpl,调用SystemServiceRegistry.getSystemService,通过键值对的方式获取PhoneLayoutInflater对象,从中我们也看到,这种方式通过键值对的方式缓存起这个对象,避免创建过多的对象,这是也一种单例的设计模式。

    现在咱们来看一下,我们是如何使用LayoutInflater来获取View,我们先从一段小代码看看。

    我新建一个布局文件,my_btn.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:orientation="vertical"
        android:text="我是一个按钮">
    
    </Button>
    

    在布局文件中,我设置其layoutwidth与layout_height分别是填充屏幕。
    在activity的content_main.xml布局:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/content_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context="ffzxcom.mytest.toucheventapplication.MainActivity"
        tools:showIn="@layout/activity_main">
    
    
    </RelativeLayout>
    

    LayoutInflater.java:
    方法一:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        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) + ")");
        }
        //通过资源加载器和资源Id,获取xml解析器
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
    
    

    我们从代码中看到,无论是方法一,还是方法二,最终还是会调用方法二进行加载,我们就从方法二的三个参数,进行分析一下。

    @LayoutRes int resource 资源文件的Id
    @Nullable ViewGroup root 根view,就是待加载view的父布局
    boolean attachToRoot 是否加载到父布局中

    从方法一看到,其实就是在调用方法二,只是方法一的第三个传参利用root!=null进行判断而已,实际上最终还是调用方法二。

    我们先利用代码进行分析一下:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mContainer = (RelativeLayout) findViewById(R.id.content_main);
    
        View view1 = LayoutInflater.from(this).inflate(R.layout.my_btn, null);
        View view2 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, false);
        View view3 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, true);
        View view4 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer);
    
        Log.e("view1:", view1 + "");
        Log.e("view2:", view2 + "");
        Log.e("view3:", view3 + "");
        Log.e("view4:", view4 + "");
    }
    

    我们加载同一个布局文件my_btn.xml,获取到view,然后分别输出,观察有什么不一样:

    view1:: android.support.v7.widget.AppCompatButton{27f4a822 VFED..C. ......I. 0,0-0,0}
    view2:: android.support.v7.widget.AppCompatButton{14fb5dd2 VFED..C. ......I. 0,0-0,0}
    view3:: android.widget.RelativeLayout{2a6bba10 V.E..... ......I. 0,0-0,0 #7f0c006f app:id/content_main}
    view4:: android.widget.RelativeLayout{2a6bba10 V.E..... ......I. 0,0-0,0 #7f0c006f app:id/content_main}
    

    问题来了,为什么我加载同一个布局,得到的view一个是Button,一个是RelativeLayout,我们每一个分析一下:

    View1:
    LayoutInflater.from(this).inflate(R.layout.my_btn, null);
    我们看到,第二个参数root为空,也就是说实际上是调用方法二(root!=null):
    LayoutInflater.from(this).inflate(R.layout.my_btn, null,false);
    第三个参数attachToRoot 的意思是,是否把这个view添加到root里面,如果为false则不返回root,而是这个的本身,如果为true的话,就是返回添加view后的root.
    因此,view1得到的是Button.

    View2:
    LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, false);
    同上可得,第三个参数attachToRoot 为false.也就是不把这个view添加到root里面去
    因此,返回的是view2,就是Button.

    View3:
    LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer, true);
    第三个参数为true,也就是意味待加载的view会附在root上,并且返回root.
    因此,我们view3返回的是这个RelativeLayout,并且是添加button后的RelativeLayout.

    View4:
    LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer);
    根据方法一跟方法二的比较,root!=null.view4跟view3的加载是一样的,同理返回的是RelativeLayout.

    根据以上的结论我们继续往下面探究,我们通过LayoutInflater.from(this).inflate(R.layout.my_btn, null)获取到了button,再把这个Button添加到mContainer中。再观察一下效果,注意,这个按钮的布局宽高是占全屏的。

    <?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:orientation="vertical"
        android:text="我是一个按钮">
    
    </Button>
     
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mContainer = (RelativeLayout) findViewById(R.id.content_main);
        Button btn = (Button) LayoutInflater.from(this).inflate(R.layout.my_btn, null);
        mContainer.addView(btn);
    }
    
    无标题.png

    大家看到问题了吗?为什么我在my_btn.xml中设置了button的布局宽高是全屏,怎么不起作用了?难道说在my_btn.xml中Button的layout_width和layout_height起不了作用?我们从源码中看一下:

    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) + ")");
        }
        //通过资源加载器和资源Id,获取xml解析器
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
    

    通过资源管理获取xml解析器,继续往下看:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ......
            //保存传进来的这个view
            View result = root;
    
            try {
                // Look for the root node.
                int type;
                //在这里找到root标签
                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!");
                }
                //获取这个root标签的名字
                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");
                    }
                    //这里直接加载页面,忽略merge标签,直接传root进rInflate进行加载子view
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    //通过标签来获取view
                    //先获取加载资源文件中的根view
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    
                    //布局参数          
                    ViewGroup.LayoutParams params = null;
                   
                    //关键代码A
                    if (root != null) {
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            //temp设置布局参数
                            temp.setLayoutParams(params);
                        }
                    }
                      ......
                    //关键代码B
                    //在这里,先获取到了temp,再把temp当做root传进去rInflateChildren
                    //进行加载temp后面的子view
                    rInflateChildren(parser, temp, attrs, true);
                      ......
                   
                     //关键代码C
                    if (root != null && attachToRoot) {
                        //把view添加到root中并设置布局参数
                        root.addView(temp, params);
                    }
    
                    //关键代码D
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
    
            } catch (XmlPullParserException e) {
                ......
            } catch (Exception e) {
                ......
            } finally {
                ......
            }
    
            return result;
        }
    }
    
    

    在这一块代码中,先声明一个变量result,这个result用来返回最终的结果,在我们的演示中,如果inflate(resource,root,isAttachRoot)中的root为空,那么布局参数params为空,并且根据关键代码D可得,返回的result就是temp,也就是Button本身。因此在以上例子中,如果说root不为空的话,Button中声明的layout_width与layout_height起到了作用。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        mContainer = (RelativeLayout) findViewById(R.id.content_main);
    
        View view1 = LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer,false);
        mContainer.addView(view1);
    }
    
    无标题.png

    注:如果通过LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer)或者LayoutInflater.from(this).inflate(R.layout.my_btn, mContainer,true)加载视图,不需要再额外的使用mContainer.addView(view),因为返回的默认就是root本身,在关键代码C中可看到:

    if (root != null && attachToRoot) {
        root.addView(temp, params);
    }
     
    

    root会添加temp进去,在代码初始化的时候,result默认就是root,我们不需要addView,在inflate中会帮我们操作,如果我们还要addView的话,就会抛出异常:
    The specified child already has a parent. You must call removeView() on the child's parent first.

    小结:

    如果LayoutInflater的inflate中,传参root为空时,加载视图的根view布局宽高无效。反之根据关键代码C与关键代码A,分别对view进行设置布局参数。

    咱们来看一下activity是如何加载视图,我们从这一段代码开始:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
    }
    

    我们往setContentView继续探索,找到Activity的setContentView()

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(@LayoutRes int layoutResID) {
        //获取窗口,设置contentView
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    
    

    getWindow()实际上是获取window对象,但Window类是抽象类,具体的实现是PhoneWindow,我们往这个类看看:
    PhoneWindow.java:

    @Override
    public void setContentView(int layoutResID) {
        //判断mContentParent是否为空,如果为空,创建
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            // 清空mContentParent 所有子view       
            mContentParent.removeAllViews();
        }
    
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           ……
        } else {
            //通过layoutInflate加载视图         
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ……
    }
    
    

    activity加载视图,最后还是通过LayoutInflater进行加载视图,activity的界面结构如下:

    无标题.png

    我们的mContentView就是ContentView,因此通过LayoutInflater加载视图进入ContentView。而root就是mContentView,因此我们在Activity不需要自己addView().

    总结:
    知其然不知其所以然,这对于LayoutInflater描述再合适不过了,文章中本来还涉及到了关于如何使用LayoutInflater中遍历view,代码太长就不一一展示,而且在inflate中我们可以看到,通过使用merge标签,可以减少view的层级,直接把merge标签内的子view直接添加到rootview中,因此布局优化能提高视图加载的性能,提高效率。还有获取LayoutInflater的方式,通过键值对进行缓存LayoutInflater,这是在android中单例设计的一种体验。

    相关文章

      网友评论

      • 二七头头:补充一点:LayoutInflater的实例还可以通过Activity的getLayoutInflater获得。
      • d60971551f30::+1:
        沈敏杰:@gggl 谢谢支持:smiley:
      • 29a50bd67c66:还有View.inflater呢😏😏
        沈敏杰:@魂之挽歌丶 /**
        * Inflate a view from an XML resource. This convenience method wraps the {@link
        * LayoutInflater} class, which provides a full range of options for view inflation.
        *
        * @param context The Context object for your activity or application.
        * @param resource The resource ID to inflate
        * @param root A view group that will be the parent. Used to properly inflate the
        * layout_* parameters.
        * @see LayoutInflater
        */
        public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
        LayoutInflater factory = LayoutInflater.from(context);
        return factory.inflate(resource, root);
        }
        最终还是调用LayoutInflater进行加载视图.
        感谢您的指出。
      • 妙法莲花1234:感谢分享
        沈敏杰:@追风917 相互学习:smile:

      本文标题:探索Android视图加载器LayoutInflater

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