Activity setContentView背后的一系列源码分析

Posted 安卓开发-顺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Activity setContentView背后的一系列源码分析相关的知识,希望对你有一定的参考价值。

本文将涉及到Activity、PhoneWindow、DecorView、LayoutInflater的相关源码分析。

下面从Activity onCreate 中的 setContentView开始

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    

进入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();
    

如果你的Activity继承自AppCompatActivity,则是这样

    @Override
    public void setContentView(@LayoutRes int layoutResID) 
        initViewTreeOwners();
        getDelegate().setContentView(layoutResID);
    

二者虽然源码看起来不一样,但核心流程是一样的,都是先把DecorView准备好,在把我们的布局给加载进来,所以这里只以Activity为例进行分析。

我们跟进到getWindow().setContentView(layoutResID) 里面,getWindow得到的是Window的子类PhoneWindow,所以具体实现要看PhoneWindow里面的setContentView方法

444行 的mContentParent就是用来承载我们写的布局的view,mContentParent的父布局则是DecorView,这里先上一张图把层次结构先明确一下

                                           

图中的FrameLayout(R.id.content) 就是这里的mContentParent,整个页面首次展现时mContentParent必然是空的,所以会执行445行 installDecor(),通过方法名就可以知道,这是要把DecorView给准备好。

 2683行只是把DecorView给new出来

DecorView继承自FrameLayout

DecorView具体长什么样子,还要继续看PhoneWindow的2693行

mContentParent = generateLayout(mDecor); 进入到generateLayout方法

protected ViewGroup generateLayout(DecorView decor) 
        // Apply data from current theme.

        //...省略一堆window样式判断、悬浮、透明、actionbar等等

        // 下面是关键代码,为DecorView挑选合适的布局layoutResource的最终赋值就决定了DecorView长什么样子
           
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) 
            if (mIsFloating) 
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
             else 
                layoutResource = R.layout.screen_title_icons;
            
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
         else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) 
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = R.layout.screen_progress;
            // System.out.println("Progress!");
         else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) 
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) 
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
             else 
                layoutResource = R.layout.screen_custom_title;
            
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
         else if ((features & (1 << FEATURE_NO_TITLE)) == 0) 
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) 
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
             else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) 
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
             else 
                layoutResource = R.layout.screen_title;
            
            // System.out.println("Title!");
         else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) 
            layoutResource = R.layout.screen_simple_overlay_action_mode;
         else 
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        

        mDecor.startChanging();
        //选中以后就把这个布局通过LayoutInflater 加载给DecorView 
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        //这个 ID_ANDROID_CONTENT就是R.id.content,
        //这个id在哪个布局文件里定义的呢?就在layoutResource对应的布局里面,我们可以随便打开一个布局看下,比如screen_simple.xml,(上面提到的这些布局在android.jar --- res ---layout 中可以找到以screen_开头)
        //通过findViewById 得到的contentParent 就是上面的mContentParent啊, 看到了吧它就是DecorView布局中的一部分。
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) 
            throw new RuntimeException("Window couldn't find content container view");
        

      
        mDecor.finishChanging();
        //返回此布局
        return contentParent;
    

此方法干了两件事:

(1)为DecorView找到一个合适的布局

这些布局文件在android.jar --- res ---layout里面,在AS中和android.jar并列的res文件夹下就可以看到布局源码,我们随便看一个screen_simple.xml

 这些screen_开头的布局都是DecorView对应的布局,系统会根据各种样式、相关设置来选择对应的布局,每一个布局里面都会有一个FrameLayout,其id是@android:id/cotent,

(2)从DecorView对应的布局文件中通过findViewById找出mContentParent

 到这里DecorView就准备好了,接下来该把我们写的布局加载进来了

 456行稍微说明一下,这个是针对设置了场景转换属性去做的一些动画效果,最终还是会用过LayoutInflater的inflate方法来加载布局,这里不跟进去了,我们下面进入到LayoutInflater部分的分析,进入inflate方法:

    /**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * @link InflateException if there is an error.
     *
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy.
     * @return The root View of the inflated hierarchy. If root was supplied,
     *         this is the root View; otherwise it is the root of the inflated
     *         XML file.
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) 
        return inflate(resource, root, root != null);
    

调用了 inflate(resource, root, root != null);方法

532行是系统做的优化,预编译,最终还是通过XmlPull解析来解析,这个解析布局文件的过程不是本文分析的重点,这里不展开去看。

下面来重点说下此方法的三个参数和返回值:resource、root、attachToRoot

第一个参数:resource是我们传入的布局文件,这个没啥好说的

第二个参数root和第三个参数attachToRoot,这两个参数要配合来使用,有这几种情况

(1)root == null :                                                                                                                        此时无论attachToRoot是否为true都不在重要,这种情况会把resource解析成view并返回,不给view设置任何LayoutParams(这意味着我们在根布局设置的一系列宽、高、padding、margin等等属性都是无效的,压根不会用)

(2)root !=null 并且 attachToRoot 是 true

这种场景正是我们setContentView使用的场景,这种场景多是系统内部在使用,此时会给解析出来的View设置LayouParams并且把view添加到root中,最后返回root。

(3)root != null 并且 attachToRoot 是 false

这种场景会给解析出来的View设置LayouParams并且不把view添加到root中,最后返回该view。

以上就是三种情况的分析,所以如果我们如果仅仅想把布局加载成view的话,不传root即可,但是还想要跟布局上设置的属性的话就传root,把attachToRoot设置为false即可。

下面从源码中去找到这三种情况:

先进入到538行 return inflate(parser, root, attachToRoot);

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 
        synchronized (mConstructorArgs) 
            //...省略若干

            //得到xml转换后的attrs
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            
            // 返回结果result 默认是root
            View result = root;

            try 
                //...省略若干

                if (TAG_MERGE.equals(name)) 
                    //...处理merge的情况
                 else 
                    //这个temp就是根据我们的布局中的根布局对应的view
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) 
                        
                        // 为temp创建 params 条件是root != null
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) 
                            //这里就是第三中情况 root!=null 并且 attachToRoot为false
                            temp.setLayoutParams(params);
                        
                    


                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    //这是第二种情况 就是我们setContentView要执行的代码
                    if (root != null && attachToRoot) 
                        root.addView(temp, params);
                    


                    //这种情况就返回temp 就是view自身
                    //第一种情况 属于这种
                    if (root == null || !attachToRoot) 
                        result = temp;
                    
                

             catch (XmlPullParserException e) 
                //...省略若干
             catch (Exception e) 
                //...省略若干
             finally 
               //...省略若干
            

            return result;
        
    

三种情况都写在上面源码的注释里了。

下面具体分析下xml布局的解析过程,以及所有view通过getParent得到的ViewParent是何时被赋值 的:

在上面inflate(parser, root, attachToRoot);这个方法中有一行代码:

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

这里的temp就是我们写的xml布局的根view,拿到这个根view以后往下走有行代码:

rInflateChildren(parser, temp, attrs, true);

这是去加载我们根view里面的子view,这里把temp给传了进去,继续跟进去看

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException 
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    

又调用了rInflate方法:

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException 

        //获取当前层级
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        //循环逐层解析我们的xml布局
        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();

            if (TAG_REQUEST_FOCUS.equals(name)) 
                //处理 requestFocus
                pendingRequestFocus = true;
                consumeChildElements(parser);
             else if (TAG_TAG.equals(name)) 
                //处理Tag标签
                parseViewTag(parser, parent, attrs);
             else if (TAG_INCLUDE.equals(name)) 
                //处理include标签
                if (parser.getDepth() == 0) 
                    throw new InflateException("<include /> cannot be the root element");
                
                parseInclude(parser, context, parent, attrs);
             else if (TAG_MERGE.equals(name)) 
                //merge标签不能出现在这里,这里是xml根里面的内容,merge只能作为根标签
                throw new InflateException("<merge /> must be the root element");
             else 
                //通过createViewFromTag方法创建出当前view,比如把<RelativiLayout .../>
                //转换成一个Java对象RelativiLayout
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //递归调用 去解析该标签下面的view
                rInflateChildren(parser, view, attrs, true);
                 //把解析出来的view添加到当前的父类中
                viewGroup.addView(view, params);
            
        

        ...
    

以上代码有几处要注意:

(1)parse.getDepth是得到的是当前位置的层级,例如:

        <RelativiLayout      --------- getPepth = 1

                 <TextView   --------- getDepth = 2

        </RelativeLayout>  --------- getPepth = 1

(2)parse.next得到的下一个标签的类型标示,例如 :

        "<" 开头的是XmlPullParser.START_TAG

        "</" 开头 或者是 "/>" 结尾的 是XmlPullParser.END_TAG

(3)rInflateChildren(parser, view, attrs, true);
        这里递归调用 去解析该标签下面的子view,如果当前已经是最底层的基础元素了比如是<TextView .../> 那么调用此方法传入的view就是TextView,但此时parser已经指向了 "<“位置,那么 parser.next得到的就是 “/>” 就是END_TAG因此不满足while循环条件,直接就结束了。     

继续看下如何递归调用的:rInflateChildren

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException 
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    

又调回了rInflate方法,这就是其解析思路。

接下来我们看下View的mParent变量是何时给赋值的:

为什么我们通过getParent方法就可以得到其父view呢?(这里的父view指的是他上级的view,不是上上级或者继续上面级的view)

我们看到执行完 rInflateChildren后,紧接着执行了viewGroup.addView(view, params);方法

这个方法我们并不陌生,平时也经常用,下面我们跟进去看下是如何添加的

ViewGroup:

    @Override
    public void addView(View child, LayoutParams params) 
        addView(child, -1, params);
    

继续跟进:

    public void addView(View child, int index, LayoutParams params) 

        // 刷新布局
        requestLayout();
        // 刷新背景等与尺寸无关的属性
        invalidate(true);
        addViewInner(child, index, params, false);
    

跟进到addViewInner

 private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) 

        addInArray(child, index);

        // tell our children
        if (preventRequestLayout) 
            //关键代码 在此处给view的parent赋值
            child.assignParent(this);
         else 
            child.mParent = this;
        
        if (child.hasUnhandledKeyListener()) 
            incrementChildUnhandledKeyListeners();
        

 找到了就在此处赋值,继续跟进:child.assignParent(this);

 View.java

protected ViewParent mParent;    
...
void assignParent(ViewParent parent) 
        if (mParent == null) 
            mParent = parent;
         else if (parent == null) 
            mParent = null;
         else 
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        



...
public final ViewParent getParent() 
     return mParent;

结论已经很明显了,是在ViewGroup的addView --> addViewInner 方法中给view的parent变量赋值的。

最后总结下整个分析流程:

Activity:setContentView --- >

PhoneWindow:setContentView --- >

PhoneWindow:installDecor --- >  准备DecorView

PhoneWindow:installDecor:generateDecor --- > new 出DecorView

PhoneWindow:installDecor:generateLayout --- > 为DecorView准备布局,然后通过findViewById从对应布局中找出 mContentParent

PhoneWindow:setContentView:461行进行inflate --- >

LayoutInflater:inflate(int resuource,ViewGroup root) --- >

LayoutInflater:inflate(int resuource,ViewGroup root,boolean attachToRoot) --- > 加载布局的过程

LayoutInflater:rInflateChildren(...) --- > 递归加载所有view

ViewGroup:addView() --> addViewInner()--- > 添加子view并给子view的parent变量赋值

彩蛋:

布局加载完后会通过getCallback得到一个cb,然后回调其onContentChanged()方法,这个callback就是我们写的Activity

 在Activity源码中的attach方法中会调用phoneWindow的setCallBack方法,传入this

关于Activity的attach是什么时候执行的,那就是一个新篇章了。。。请看我的这篇文章

Android 11(platfrom 30)APP启动流程(含Activity)核心点记录

以上是关于Activity setContentView背后的一系列源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Activity的setContentView的流程

setContentView()给当前Activity加载布局出错

Android-Activity中setContentView流程解析

Android源码解析Activity#setContentView()方法

在 setContentView(R.layout.activity_second) 中,activity_second 为红色

将 View 从 setContentView 传递给非 Activity 类