Android成长之路之layout加载过程

    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("<merge /> 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();
 
            //处理<requestFocus />, <include />, <merge />, <blink />标签的情况
            ....
            // 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的整个视图便组织完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值