谈谈Activity的setContentView是怎么加载XML视图的

不管你是一个刚接触Android开发的新手,还是一个深耕多年的老司机,打开AS新建一个项目开发APP,第一件事就是打开MainActivity,在onCreate方法里调用setContentView,将xml布局索引值传进去。

很多人其实都没注意到这个方法是怎么一个逻辑,就是按照我们当时学习android开发的视频中老师说的,把布局文件放进去,我们的activity就有页面了;其实我在很长一段时间也是这个逻辑,没有想太多,就直接用就行了,直到有一天一个面试题到了我面前,这个方法是怎么执行的?说实话当时有点乱,平时还真没注意过这个方法,一时不知道说什么好,尴尬!!!

今天就来了解下它的神秘之处。

我们使用AndroidStudio新建一个项目,无须添加任何代码,就打开MainActivity,首先我们想到既然setContentView是给Activity设置contentview的,那你对这个activity的整个页面的View结构有什么概念呢?

我们点击AndroidStudio的顶部菜单栏的Tools菜单,依次点击里面的Android》Layout Inspector;这样就会弹出一个展示Activity窗口视图结构的窗口出来,如图
这里写图片描述

当前activity的布局里只添加了一个textview,并且当前Activity在setContentView之前没有进行Features设置。

当用鼠标放在左边具体view上面,右边视图上会展示相应的范围

  1. 从这里可以看出来Activity最根部的view是DecorView,后面我们会说到DecorView其实是一个FrameLayout
  2. DecorView包含两部分,一个装载内容的LinearLayout和一个顶部View
  3. LinearLayout包含一个ViewStub和FrameLayout,其实就是一个action_bar,只不过使用ViewStub来修饰,因为开发者是可以设置不要标题栏的;至于FrameLayout就是真正装载我们通过setContentView设置进去的View
  4. 最后这个TextView就是我们自己布局的内容

结构了解之后再回来通过setContentView去探索它是怎么形成的:

Activity setContentView

通过在onCreate中点击setContentView方法进入到源码中查看

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
}

这里先调用getWindow()方法

public Window getWindow() {
        return mWindow;
}

这个mWindow是activity类的全局变量 private Window mWindow;它是在attach方法中实例化的,并且实现了Callback接口,这个后续会用到

mWindow = new PhoneWindow(this, window);
mWindow.setCallback(this);

可以看到其实是实例化了一个PhoneWindow对象,Window是个什么东西呢,可以这样说,它是顶级窗口外观和行为策略的抽象基类。 应该将此类的实例用作添加到窗口管理器的顶级视图。 它提供标准的UI策略,例如背景,标题区域,默认密钥处理等。此抽象类的唯一现有实现是android.view.PhoneWindow。

PhoneWindow setContentView

回到setContentView方法,获取窗口实例后,调用PhoneWindow的setContentView方法:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
              cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

我们第一次进来的时候,这个mContentParent(这个就是装载我们设置的布局view的容器)肯定是null,那么就进入installDecor方法,这个方法很长,就不全部贴上来了,贴关键部分

if (mDecor == null) {
            //通过new 创建Decorview
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //根据mDecor实例获取mContentParent
            mContentParent = generateLayout(mDecor);

第一步:如果DecorView为null,就通过generateDecor来构造一个实例,看看DecorView这个类

public class DecorView extends FrameLayout

说明这就是布局控件,也表明Activity的根布局就是一个FrameLayout

第二步:根布局有了,就需要往里面塞东西了,这时候就判断如果mContentParent 为null,就通过generateLayout来构建,我们看看这个方法,这个方法很长,我把一些类似的代码删了

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

        //根据Manifest里设置的theme来进行相应设置
        TypedArray a = getWindowStyle();

        if (false) {
            System.out.println("From style:");
            String s = "Attrs:";
            for (int i = 0; i < R.styleable.Window.length; i++) {
                s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
                        + a.getString(i);
            }
            System.out.println(s);
        }

        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                & (~getForcedWindowFlags());
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        } else {
            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
        }

        //比如主题里设置了notitle,这里就会进行设置
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }
        .
        .
        .
        .
        // Inflate the window decor.
        //添加布局到DecorView,前面说到,DecorView是继承与FrameLayout,它本身也是一个ViewGroup,
        // 而我们前面创建它的时候,只是调用了new DecorView,此时里面并无什么东西。
        // 而下面的步骤则是根据用户设置的Feature来创建相应的默认布局主题。举个例子,
        // 如果我在setContentView之前调用了requestWindowFeature(Window.FEATURE_NO_TITLE),
        // 这里则会通过getLocalFeatures来获取你设置的feature,进而选择加载对应的布局,
        // 此时MainActivity没有进行任何设置,对应的就是R.layout.screen_simple
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else 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();
        //将上述确定的布局添加到mDecor中 mDecor其实是一个FrameLayout
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //contentParent是mDecor布局中的一个子view FrameLayout id是content
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        if (getContainer() == null) {
            final Drawable background;
            if (mBackgroundResource != 0) {
                background = getContext().getDrawable(mBackgroundResource);
            } else {
                background = mBackgroundDrawable;
            }
            mDecor.setWindowBackground(background);

            final Drawable frame;
            if (mFrameResource != 0) {
                frame = getContext().getDrawable(mFrameResource);
            } else {
                frame = null;
            }
            mDecor.setWindowFrame(frame);

            mDecor.setElevation(mElevation);
            mDecor.setClipToOutline(mClipToOutline);

            if (mTitle != null) {
                setTitle(mTitle);
            }

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }
            setTitleColor(mTitleColor);
        }

        mDecor.finishChanging();

        return contentParent;
    }

方法很长,我们从头看,

  1. 首先调用getWindowStyle()方法,这个就是获取我们在AndroidManifest里对activity设置的Theme,然后做相应的设置;比如主题里设置了windowNoTitle,那这里就会设置不要标题栏
  2. 接下来会调用getLocalFeatures()方法,我们有时候会在activity的setContentView方法前设置requestWindowFeature(Window.FEATURE_NO_TITLE)类似的属性,这个方法就是获取这些设置的并做相应的处理;这也说明了为什么我们要在setContentView之前调用这些设置方法了
  3. 我上面的例子没有做相关设置,if-else语句最终会走到最后一个layoutResource = R.layout.screen_simple;这个布局文件就会通过下面的 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)方法被添加到DecorView中,看看这个布局代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

这个布局是不是跟我最上面贴的activity的视图结构一样的,这样之前构建的DecorView这个ViewGroup里就有内容了
4. 添加完之后就要获取这个布局里面的view拿来用了,然后就有这句

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

这样就拿到了布局里的FrameLayout并赋值给contentParent,最后把这个contentParent返回

第三步:mContentParent 这样就有值了,installDecor就基本上结束了

最后再回到setContentView方法

通过installDecor方法构建了一个DecorView,这个是作为Activity的根部View,并且添加了一个布局到DecorView里;通过id找到布局里的FrameLayout赋值给mContentParent ,这个ViewGroup就是用来给我们放置要设置的布局的

接下来会执行到mLayoutInflater.inflate(layoutResID, mContentParent);第一个参数就是我们传入的布局id,第二个参数就是上面构建的FrameLayout。

这个inflate方法最终会走到下面

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();
        }
    }
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            //记录解析日志
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            //通过parser得到layout中的所有view的属性集保存在attrs中
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            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!");
                }
                //得到layout的节点,例如,view、merge、include等
                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 {
                    // 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);
                        }
                        // 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);
                        }
                    }

                    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) {
                        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) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

这方法逻辑就是通过createViewFromTag方法不断解析并创建出对应的view,然后通过root.addView(temp, params)方法,就将我们编写的布局添加到了mContentParent 这个FrameLayout布局中。

最后就是通过cb.onContentChanged()来通知activity布局内容已经变化了;在最上面我们知道Activity的attach方法中实例化PhoneWindow的时候注册了这个接口,并实现了onContentChanged方法

public void onContentChanged() {
}

显然这是一个空的实现,所以我们可以在我们自己的Activity里重写这个方法来监测页面布局的变化。

到这里setContentView方法就分析结束了,做个总结

  1. Activity启动后在attach方法中会实例化一个PhoneWindow对象,说明每个Activity都会拥有一个Window窗口,并且界面展示的特性是由Window控制
  2. 我们的Activity通过setContentView方法设置页面内容,会走到PhoneWindow类的setContentView方法中
  3. 在setContentView方法中,会实例化一个DecorView,这是每个Activity根布局,并根据Theme,Feature往这个布局里添加了一个LinearLayout
    4.LinearLayout布局里有title和FrameLayout,把FrameLayout作为mContentParent来存放我们添加的ViewGroup或者View
  4. 最后通过Callback来通知activity页面布局更新

最终就是这张图,网上借鉴
这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_30993595/article/details/80915131