×

Android LayoutInflater列传

96
肱二头肌的孤单
2016.09.09 19:51* 字数 3375

由LayoutInflater谈起

layoutInflater.inflate(int resource, ViewGroup root)
layoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)
还记得这2个方法吧,刚看的时候,是不是比较头痛了?
LayoutInflater,刚学安卓就会遇到的东西,刚开始时,不明白含义,只能死记硬背。但是我这个人不理解的话,记忆起来的记忆力比较差,可能是死记硬背的太用力,导致后来看到这个类,竟然隐隐约约的有些头痛。T_T。从头梳理一下把。

UI,这个词,听到无数次了。User Interface,用户界面,咱手机又不是座机,就算是座机,那拨打电话的数字按键,那也是属于UI啊,别说智能机了。

官方文档 (SDK的training部分的Building a Simple User Interface章节)

The graphical user interface for an Android app is built using a hierarchy of View and ViewGroup objects.
(Android 的应用程序的图形用户界面是使用view和viewgroup的树形结构来生成的。)

Android provides an XML vocabulary that corresponds to the subclasses of View and ViewGroup so you can define your UI in XML using a hierarchy of UI elements.
(android提供了xml的词汇,这些词汇基于view和viewgroup,用这些词汇你可以在xml中来定义你的UI)

既然官方都发话了,要使用view和viewgroup来显示UI,那还犹豫什么,用呗~
但是这些元素要显示在哪里呢?大声告诉我,没错,作为android四大组件之一,Activity闪亮登场。

官方文档 (SDK Reference部分-->android.app.activity章节)

An activity is a single, focused thing that the user can do. Almost all activities interact with the user, so the Activity class takes care of creating a window for you in which you can place your UI with setContentView(View).
一个activity(活动)是一个单一,集中的且被用户处理的事物。几乎所有的activity都会和用户相互作用,交流(交互),所以activity类专注于创建一个窗口,这个窗口通过setContentView(View)来放置你的UI(view和viewgroup)

首先,一个应用要显示的话,要通过activity,但是,activity的翻译是活动啊,活动跟显示的关系不大啊。其实,activity是一个控制单元,即可视的人机交互界面。而真正显示的部分(看上面的文档),是activity会创建一个窗口(window),然后这个窗口调用了setcontentview的方法来显示UI(view和viewgroup)。

打个比喻: (引用于 Android_View,ViewGroup,Window之间的关系)
Activity是一个工人,它来控制Window;Window是一面显示屏,用来显示信息;View就是要显示在显示屏上的信息,这些View都是层层重叠在一起(通过infalte()和addView())放到Window显示屏上的。而LayoutInfalter就是用来生成View的一个工具,XML布局文件就是用来生成View的原料

setContentView()又是怎么工作的呢?这又要牵扯到窗口的构成了,走你~

activity窗口的构成

image

看下源码:
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) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
  1. 实际上是调用了getWindow().setContentView(layoutResID);的方法。
    getWindow()就是得到了上图中的PhoneWindow对象,DecoView则是一个窗口的根视图,包含TitleView和ContentView。
    TitlView就是标题栏了,还记得之前去掉标题栏把requestWindowFeature(Window.FEATURE_NO_TITLE);这里隐藏的就是TitleView了。
    setContentView()则是对于上图中的contentview进行设置(当然了,名字都是以一毛一样的,这里的setContentVIew中的contentView,就是指的上图的Contentview,有点点绕口了。。)这里的contentview,就是我们设置xml的主力战场啦。
  2. 好吧,解释完了PhoneWinDow和titleView,ContentView的关系,接着走
    源码
  public Window getWindow() {
        return mWindow;
    }

接着上文,getWindow()方法返回了一个mWindow对象,所以,setContentview()里面实际上也是调用的这个mWindowsetContentView()的方法,那么,这个mWindow对象是从哪里来的呢?想肯定想不出来,继续看源码~

  1.   +_+....终于找到啦!!  请看    
    
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this);   //--------------分割线要长才能被看到~我在这里,吼吼吼!!
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
       ...
    }

上面的代码挺多的,其实,关键的代码,就是一行,就是我在代码中标注的地方。(就一行标注的地方,好找~)

在activity里的attach方法里,终于是找到了mWindow被赋值的地方了。
至此,activity设置setContentView总算告一个段落了。

但是,有些好奇的宝宝又要发问了,这个attach方法是在哪里运用的呀。activity基本的生命周期没有这个方法呀。而且上面的attach方法又是声明final的,也不能覆盖,咋整哩。反正我当时是没想明白,于是又陷入了纠结中。。。

于是在网上慢慢搜索,终于发现了老罗的一系列文章,简直令人豁然开朗、茅塞顿开.

Android应用程序窗口(Activity)的窗口对象(Window)的创建过程分析 这有一系列,慢慢的看,收获很多啊。悄悄的告诉你,精华在每篇文章的结尾总结部分哦。前面的代码看着头痛的,留着慢慢看,可以先看下总结。 下面摘取下我们比较关心的部分吧~

好了,回到主题,attch到底在哪里?
这又要跟activity的创建扯上关系了。大家都知道,activity的生命周期,onCreateonDestroy(),但是作为建立在java语言上,在java中,咱平时创建对象,

  1. 要么是new创建对象
  2. 要么是通过反射创建对象,Class.newInstance()方式,
  3. 要么clone(),
  4. 要么运用反序列化

其实,见得最多的,就是前2种了,那么activity对象的产生,到底是怎么产生的呢?是new出来的?是反射吗?
虽然说系统帮咱都做好了,但是没有看见创建的语句,老是觉得心里有点慌慌哒。看到老罗的博客,终于找到啦。
以下内容摘自Android应用程序窗口(Activity)的运行上下文环境(Context)的创建过程分析

image

注意上图中的第二步和第六步
2:new Activity

public class Instrumentation {  
    ......  
  
    public Activity newActivity(ClassLoader cl, String className,  
            Intent intent)  
            throws InstantiationException, IllegalAccessException,  
            ClassNotFoundException {  
        return (Activity)cl.loadClass(className).newInstance();  
    }  
  
    ......  
}  

没错!原来activity是通过反射来进行实例化的!!好吧,心安了~
6: attach
这里就是我们一直魂牵梦绕的attach了,在这个方法里,我们对activity的实例进行了初始化,包括得到了我们的mWindow对象。

总结下:

  1. 一个Android应用窗口的运行上下文环境是使用一个ContextImpl对象来描述的,这个ContextImpl对象会分别保存在Activity类的父类ContextThemeWrapper和ContextWrapper的成员变量mBase中,即ContextThemeWrapper类和ContextWrapper类的成员变量mBase指向的是一个ContextImpl对象。

  2. Activity组件在创建过程中,即在它的成员函数attach被调用的时候,会创建一个PhoneWindow对象,并且保存在成员变量mWindow中,用来描述一个具体的Android应用程序窗口。

  3. Activity组件在创建的最后,即在它的子类所重写的成员函数onCreate中,会调用父类Activity的成员函数setContentView来创建一个Android应用程序窗口的视图。

说了这么多,跟LayoutInflater也没关系啊。
前面的过程只是说了下窗口诞生的大概过程,之前在官方文档里,说了用xml来定义ui,说了界面是有view和view group构成的,那么,到底是怎么由xml转化为对象跑在程序中的呢?是怎么由setContentView化为图形界面的呢?

LayoutInflater表示,终于到我的出场机会了。
先到官方文档里走一遭

Instantiates a layout XML file into its corresponding View objects. It is never used directly. Instead, use getLayoutInflater() or getSystemService(Class) to retrieve a standard LayoutInflater instance that is already hooked up to the current context and correctly configured for the device you are running on.
实例化一个布局的xml文件为一个和它相对应的view对象。它不会直接使用,相应的,我们使用getLayoutInflater()getSystemService(Class)来得到一个LayoutInflater的实例,这个实例已经和当前的context绑定好了。

其实,LayoutInflater主要用来加载布局,而在setContentView()里,最终也是用到了LayoutInflater。

LayoutInflater,有3种获取方式

  1. LayoutInflater layoutInflater = LayoutInflater.from(context);(其实第一种就是第二种的简化)
  2. LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  3. LayoutInflater layoutInflater = Activity.getLayoutInflater();(第三种是要在activity里才有对应方法)

而其常见的用法有2种

  1. layoutInflater.inflate(int resource, ViewGroup root)
  2. layoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)

当时看到这个用法,头是很大的啊,第一个参数是传的xml的id,这都没问题,最让人害怕的,就是后2个参数,到底是嘛意思嘛!
哎,看到就烦。

看看第一种方式源码

 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

其实就是调用了第二种方式,如果roota==null,就相当于inflate(id,root,false),如果不为null,就相当于inflate(id,root,true)

所以,我们只要看第二种就好了。

   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();
        }
    }

这里提到了inflate(parser, root, attachToRoot),跟进去

 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;
            
            1.注意啦!!!非常核心的一步,先把要返回的view用root来赋值,为什么这样做哩,下面就知道了。
            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 {
                
                    2. 注意啦!!!我在这里额,下面的createViewFromTag会返回xml布局里的最外层view。
                    // 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);
                        }
                        
                        3. 注意啦!!!如果传的root不为null的时候,得到布局参数。
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                        
                        4.注意啦!!!不被绑定的时候,而root又不为空,会把xml的view的参数设置
                            // 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) {
                    5.注意啦!!! 如果root不为空,且第三个参数为true的时候,把这个xml渲染得到view作为子view加到root里去。
                        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) {
                    
                    6.注意啦!!!当root为空,或者(是或者哟!)第三个参数为空的时候,把返回的view用xml的view来赋值,
                    注意标注点1的时候,是一进来就把result用root来赋值哟。
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (Exception e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                                + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);

            return result;
        }

先得出结论,再看下文分析
ayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot)
对于这3个参数的理解

  1. root为空的情况,这个方法返回的是resource所渲染的最外层根view,(别忘了UI是建立在树形结构上的,如果有子view的话,当然也包含在最外层根view里了。),虽然标注1是把返回的view用root来赋值,但是请注意标注6,由于root等于空,所以又用temp(这个是resource这个xml所渲染的最外层view)来代替了。
  2. root不为空,并且attachToRoot为true的情况下,返回的是root,root!呀!!标注1把返回的view用root赋值,然后标注5,把temp(resource这个xml的最外层view)作为子view添加到root里去,后来再无修改,所以,返回的result还是等于root,只是这个root里包含了xml所渲染的view。
  3. root不为空,且attachToRoot为false,注意标注6,返回的是xml渲染的view,但是在标注3里得到了root的布局参数(layoutparams),在标注4中, temp.setLayoutParams(params)进行赋值。所以这个view是有了layoutparams的view。得到了root的内力传输。。。

注意下标注2,view的产生,这里就是由xml转化为对象的一步了,跟进去,最终我们会发现

 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;

    ...

            final View view = constructor.newInstance(args);
     ...
     }

在这个方法了,也是通过反射的方式得到了view的对象.而且,android是通过pull方式来解析xml的哦.

彩蛋:注意标注3是吧params = root.generateLayoutParams(attrs);,使用过自定义viewgroup的童鞋们是不是有点眼熟了?自定义LayoutParams是要重写几个方法还记得吧~当时我是找要传参AttributeSet这个方法时找了好久,没想到在这里哦。具体的内容,我会在下一篇自定义viewgroup的时候详细谈谈。

所以,上面的root是否为空的主要区别,就是有没有得到root的layoutparams.
当然,layoutParams又有什么用?,就算我是上述的情况2,得到了layoutparams又有什么用啊?还是不明白.
下面来走个例子~

一个简单的TextView的布局

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="200dp"
    android:background="#deb887"
    android:orientation="vertical"
    android:text="好好学习天天向上">
</TextView>

注意这里的最外层布局啊~

activity的布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rl_main"   //注意,id搁这了.
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.tx.laoluo.activity.TestLayoutButtonActivity">

</RelativeLayout>
public class TestLayoutTextViewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_layout_button);
        RelativeLayout rl_main = (RelativeLayout) findViewById(R.id.rl_main);
        View textView = getLayoutInflater().inflate(R.layout.view_button,null);
        rl_main.addView(textView);
    }
}

这里Layout.inflater是第一个种方式,root为null的情况

看看结果

image

属性完全没用啊.说好的android:layout_width="300dp"
android:layout_height="200dp"呢?

这是为什么,又要看addview的源码了...
addview最终会来到这个方法里

    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }

如果 child.getLayoutParams()为空的话,则会生成一个默认的layoutparams,自己的属性,就不会生效了.至于为什么,这要牵扯到view的绘制了.关于view,viewgroup,laytoutparams他们之间的爱恨情仇,相爱相杀,这又是另一个故事了...这个故事具体的将在下篇博客里为大家娓娓道来.

那麽,让我们打印一下.注意,这个打印要在addview前,不然,后面就有默认的了.

     RelativeLayout rl_main = (RelativeLayout) findViewById(R.id.rl_main);
        View textView = getLayoutInflater().inflate(R.layout.view_button,null);
        Log.d("taxi","textView.getLayoutParams()="+textView.getLayoutParams());
        rl_main.addView(textView);

结果
09-09 11:00:07.504 15425-15425/com.example.admin.myapplication D/taxi: textView.getLayoutParams()=null
ok,果然为空.
让我们看看另外2种方式
root不为空,attachtoroot为false

View textView = getLayoutInflater().inflate(R.layout.view_button,rl_main,false);
rl_main.addView(textView);

结果

image

ok,生效了.

第三种方式
root不为空, attachtoroot为true

        View textView = getLayoutInflater().inflate(R.layout.view_button,rl_main,true);
//        rl_main.addView(textView);

结果

image

ok,依然生效.
注意,这里由于是true,在渲染时,会默认把咱们的textview作为子view添加到rl_main中.不用在addview了,不然会报错.不能把一个有了parent的view再当作子view添加给别人. (throw The specified child already has a parent. You must call removeView() on the child's parent first)

ok.回顾一下吧, 本文从基本的UI显示入手,xml与显示的关系,UI与view,viewgroup的关系,然后说到activity的作用,activity的内容,窗口的概念,setContentView的概念,再引入了mWindow的概念,activity创建过程,这些都是LayoutInflater的前身,然后,由setContentView开始,真正的引入了LayoutInflater的概念,再经过源码的学习,了解到了LayoutInflater的实例化以及参数的含义.至此,咱们的LayoutInflater传就吿一段落了,至于之前上文提过的view,viewgroup,以及layoutParams的关系,那是另外的故事了,欲知后事如何,且听下回分解~

日记本
Web note ad 1