Android最重要的东西是四大组件,相信大家初学Android时都是从四大组件开始学起的。其中Activity是最先接触到的,也是用到最多的,因为它太重要了,它的职责是显示与交互,显示的重任就交给了布局文件Layout。相信大部分初学者会对xml布局文件是如何加载到Activity里成为界面视图感到好奇,甚至一些同学都没有想过这个问题,他们会说:setContentView(layoutResId), so easy! 然而忽略了这一句代码背后的故事。当我们静下来去学习他们背后的过程与原理后,你会发现过程很复杂,原理很简单,收获很丰富,感觉很棒滴!今天,我们就从最简单且用到最多的开始学习—布局文件加载之谜。

    加载布局文件有两种方式:一、setContentView(layoutResId)。二、首先获得一个LayoutInflater对象inflate,然后inflate.inflate(layoutResId)即可。这两种方式用的都很多,下面我们就以setContentView为着手点开始分析。

Step1:进入Activity源码查看setContentView方法。

public void setContentView(int layoutResID) {      getWindow().setContentView(layoutResID);  }    public Window getWindow() {      //Window对象,本质上是一个PhoneWindow对象        return mWindow;  }

其中Window是一个抽象类,mWindow是一个PhoneWindow对象,它是通过mWindow = PolicyManager.makeNewWindow(this);创建出来的。PhoneWindow是Android中的最基本的窗口系统,是Activity和整个View系统交互的接口。

Step2: 继续跟踪PhoneWindow类的setContentView方法。

public void setContentView(int layoutResID) {      //第一次调用, 则mContentParent为null,且mDecor也为null。     if (mContentParent == null) {          installDecor();      } else {          mContentParent.removeAllViews();      }      mLayoutInflater.inflate(layoutResID, mContentParent);      final Callback cb = getCallback();      if (cb != null) {          cb.onContentChanged();      }  }

首先判断mContentParent是否存在,如果是第一次调用setContentView, 那么mContentParent不存在,则调用installDecor方法创建mDecor和mContentParent对象。接着我们看到mLayoutInflater.inflate(layoutResID,mContentParent);这么一行,mLayoutInflater就是一个LayoutInflater对象,这说明我们上面讲的两种加载资源文件的方式最终都归为第二种,即将布局文件通过LayoutInflater对象转换为View树,并添加到mContentParent视图中,最后交给WindowManagerService显示。题外话,从这段代码的判断逻辑可以看出,我们可以多次调用setContentView来改变我们的界面。

Step3:为了更好的了解mContentParent这个对象,我们跟踪installDecor方法。

private void installDecor() {    if (mDecor == null) {        mDecor = generateDecor();        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);        mDecor.setIsRootNamespace(true);        ...    }    if (mContentParent == null) {        mContentParent = generateLayout(mDecor);        ...    }}

首先判断mDecor如果为空,则调用generateDecor()创建一个DecorView(该类是FrameLayout子类,即一个ViewGroup视图)。如果mContentParent为空,则生成一个mContentParent对象,mContentParent对象也是一个FrameLayout视图,其过程是通过窗口的风格和属性选择一个系统的布局文件,通过mLayoutInflater.inflate加载该系统布局文件,然后将id为content的FrameLayout赋值给mContentParent,这里我们还是逃不过加载布局文件。Activity视图的层级关系如下图:(此图片为引用)

由于generateDecor和generateLayout不是这次的主题,在这里就不继续深究了,我们回到Step2中mLayoutInflater.inflate(layoutResID,mContentParent);这段代码,这才是加载布局文件的开始。

Step4:进入LayoutInflater类跟踪inflate方法。

public View inflate(int resource, ViewGroup root) {    return inflate(resource, root, root != null);}public View inflate(int resource, ViewGroup root, boolean attachToRoot) {    if (DEBUG) System.out.println("INFLATING from resource: " + resource);    XmlResourceParser parser = getContext().getResources().getLayout(resource);    try {        return inflate(parser, root, attachToRoot);    } finally {        parser.close();    }}

在函数中传入resource获取到该资源文件的解析器XmlResourceParser,使用解析器parser对象来解析xml布局文件。XmlResourceParser是专门解析xml文件的解析器,小巧易用,解析速度快,读取到xml的声明返回START_DOCUMENT;读取到xml的结束返回END_DOCUMENT; 读取到xml的开始标签返回START_TAG;读取到xml的结束标签返回END_TAG;读取到xml的文本返回 TEXT。在这里不做深入的研究,有机会做一个专门的介绍。

Step5:继续进入inflate(parser, root, attachToRoot)方法。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {        synchronized (mConstructorArgs) {            final AttributeSet attrs = Xml.asAttributeSet(parser);            ...            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 (TAG_MERGE.equals(name)) {                    if (root == null || !attachToRoot) {                        throw new InflateException(" can be used only with a valid "                                + "ViewGroup root and attachToRoot=true");                    }                     rInflate(parser, root, attrs, false);                } else {                    // Temp is the root view that was found in the xml                    View temp;                    if (TAG_1995.equals(name)) {                        temp = new BlinkLayout(mContext, attrs);                    } else {                        temp = createViewFromTag(root, name, attrs);                    }                     ViewGroup.LayoutParams params = null;                     if (root != null) {                        // 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);                        }                    }                     // Inflate all children under temp                    rInflate(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 (XmlPullParserException e) {                //...            } finally {                ...            }             return result;        }}

这个方法代码比较多,但不复杂,注释也很充分,较容易理解。首先解析布局type并校验是否合法,获取布局的根节点名创建根视图,接着判断传入进来的root视图,如果不为null,则为该根视图赋值外面父视图的布局参数。然后调用rInflate函数为根视图添加所有子节点视图。

Step6:rInflate方法递归布局文件的根视图的所有子节点,将解析到的View构成视图树。

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,            boolean finishInflate) throws XmlPullParserException, IOException {         final int depth = parser.getDepth();        int type;        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();             //处理, , , 标签的情况            ....            // View节点            else {                //根据节点名构建一个View实例对象                final View view = createViewFromTag(parent, name, attrs);                final ViewGroup viewGroup = (ViewGroup) parent;                //调用generateLayoutParams()方法返回一个LayoutParams实例对象.                             final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);                //̐继续递归                rInflate(parser, view, attrs, true);                //将该View以特定LayoutParams值添加至父View                              viewGroup.addView(view, params);             }        }        if (finishInflate) parent.onFinishInflate(); }

这段代码也很容易理解,首先校验type是否合法,然后根据节点名称处理各种标签的情况,如果不是标签,则根据名称创建一个视图,以此视图为起点继续递归构建视图树。其中finalView view = createViewFromTag(parent, name, attrs),由节点名等参数构建一个view实例对象, 在此方法中查找name是否包含符号点,如果没找到则说明不含有包名,就使用默认的包名android.view.最终会进入createView来构建一个View对象。

Step7: 进入最终如何生成View对象的方法createView
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;        try {            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);            // ͨmContext.getClassLoader()4ӔخameĀ΄            clazz = mContext.getClassLoader().loadClass(                        prefix != null ? (prefix + name) : name).asSubclass(View.class);            // ͨɤõԬʽ            constructor = clazz.getConstructor(mConstructorSignature);                sConstructorMap.put(name, constructor);            ...            Object[] args = mConstructorArgs;            args[1] = attrs;            final View view = constructor.newInstance(args);            if (view instanceof ViewStub) {                // always use ourselves when inflating ViewStub later                final ViewStub viewStub = (ViewStub) view;                viewStub.setLayoutInflater(this);            }            return view;        } ...    }

这里很明显,通过mContext.getClassLoader()来加载name的类文件,然后利用反射机制实例化一个View对象,这个对象就是我们在界面上看到的一个个元素。至此,setContentView和inflate方法这两种方式加载布局文件,直到最后生成视图树的过程就完成了。视图树生成好了之后就是交给WindowManagerService来管理及显示了。

        针对上面复杂的过程,我们做一个简单的总结。布局文件是如何被显示成为视图的呢?很简单,首先PhoneWindow会生成DecorView和mContentParent,这是布局文件的显示区域。接着根据布局文件id生成一个xml文件解析器XmlResourceParser,利用此解析器来递归文件里的每个节点,根据节点名来判断是各种标签还是视图名称。然后根据名称来加载相应的类,利用反射机制生成实例,将此实例加入到父视图中,这样便形成了视图树。最后将视图树加入到mContentParent中,这样Activity的整个视图便组织完成了。

更多相关文章

  1. 一款常用的 Squid 日志分析工具
  2. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  3. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  4. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  5. eclipse安装ADT插件重启后不显示Android(安卓)SDK Manager和Andr
  6. 【Android】ImageView设置背景图片报错:Error inflating class Im
  7. Linux下Android手机刷机指南
  8. android 启动页白屏解决方案
  9. Go,onAndroid

随机推荐

  1. Android Reverse Engineering 101 – Par
  2. Android Bluetooth Architecture
  3. Android应用程序剖析
  4. Android - Custom Components
  5. 相对布局控制控件居右显示
  6. android-环境
  7. android 自定义TextView实现秒级数字时钟
  8. Android之自定义适配器
  9. android 生成密钥 签名
  10. mono for android Main.axml