美文网首页面试题Android网易课
setContentView是如何把布局加上去的

setContentView是如何把布局加上去的

作者: Fizzzzer | 来源:发表于2019-08-07 15:55 被阅读15次

    在Android开发中,最常见的代码就是setContentView,然后传入你写的布局ID,那么布局就被加载到界面中了,系统究竟是怎么被加到界面中的,就需要通过源码来查看了。

    点击setContentView方法,进去会发现调用了以下的代码

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

    可以看到是通过getWindow方法来设置布局文件的,那我们在看一下这个getWindow做了什么事情,在点击进去看一下

    /**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }
    

    其实这个getWindow方法就是返回了当前的window窗口对象,而且通过注释我们还可以知道,如果当前的Activity不可见的时候,这个window对象是为空的,那么这个window到底是什么,我们在进去看一下window类是什么样的,

    /**
    * Abstract base class for a top-level window look and behavior policy.  An
    * instance of this class should be used as the top-level view added to the
    * window manager. It provides standard UI policies such as a background, title
    * area, default key processing, etc.
     *
    * <p>The only existing implementation of this abstract class is
    * android.view.PhoneWindow, which you should instantiate when needing a
    * Window.
    */
    public abstract class Window {
     /** Flag for the "options panel" feature.  This is enabled by default. */
        public static final int FEATURE_OPTIONS_PANEL = 0;
        /** Flag for the "no title" feature, turning off the title at the top
        *  of the screen. */
        public static final int FEATURE_NO_TITLE = 1;
    

    关于window类的源码截取了一部分,可以看到这是一个抽象类,通过注释,我们获取信息,这个类有一个唯一的实现类PhoneWindow,所以接下来我们就需要主要去分析一下这个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;
    }
    

    PhoneWindow这个类中setContentView方法中,我们重要关注两个方法,一个是installDecormLayoutInflater.inflate(layoutResId,mContentParent);而且在调用installDecor方法的时候,还对mContentParent进行了判断,那这个mContentParent是什么呢,我们通过installDector方法点进去看一下

    if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
    

    由于源码里面中的方法代码太长了,这里只做部分截取,这里看到,如果mDecor==null的话,我们对mDecor进行了一个初始化,初始化的方法是generateDecor(-1),我们点击去看一下

    protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }
    

    可以看到,其实这个方法就是对DecorView做了一个初始化,最后也是返回了一个DecorView

    DecorView是window的一个顶层容器,继承自FrameLayout

    看完这个generateDecor之后,我们在回到installDecor方法,接着往下看,

    if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            ...
    }
    

    我们发现,在初始化mDecor之后,又对mContentParent进行了初始化,那么这个mContentParent是什么,在通过generateLayour(mDecor)方法来对里面的具体实现进行探索

    代码点进去有点多,在开始的时候,是通过系统内部的一些样式来进行一些特性样式的设置,这里可以略过,然后真正需要研究的代码是从注释中 Inflate the window decor开始
    可以在这里看到,源码中初始化了一个int layoutResource;那么我们往下研究,发现下面就是对这个layoutResource字段进行赋值操作。简单的理解就是通过不同的样式来加载系统中一些默认的布局文件,

    在对这个layoutResource字段进行赋值之后,我们重点关注两行代码,

    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    

    第一行代码中,主要是这个mDecor.onResourcesLoaded()方法,传入了之前赋值的layoutResource字段,这个方法点进去之后发现,其实就是对这个layoutResource指定的布局进行的绘制然后设置给了mDecor,其实也就是相当于理解为给顶层DecorView设置了一个布局,而这个布局是系统内置的,可以通过样式来指定加载哪些不同的布局文件。

    第二行代码中,发现进行了一个findViewById操作,那这个ID是什么,点击去看一下

    /**
     * The ID that the main layout in the XML layout file should have.
     */
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
    

    通过注释可以获取到信息就是,这个id是主要的入口布局ID,并且必须有,在获取到这个contentParent之后,这个方法就将这个对象进行了返回,那这里就很疑问了,为什么这个id一定含有,我们通过layouttResource字段来看看之前加载的系统的布局文件。我们以系统中的R.layout.screen_simple布局为例,发现他的布局是这样写的

    <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>
    

    可以看出,这有一个布局id为content的FrameLayout,所以其实我们通过findviewViewById来获取的控件就是这个FrameLayout,通过查看系统中其他的布局文件,我们都能发现有一个ID为content的FrameLayout控件。所以是shuld have

    那么这里我们一层层的返回,就会发现,其实PhoneWindow类中的mContentParent就是DecorView中的一个FrameLayout,在回到setContentView方法中,在对mContentParent进行初始化完成后,调用了mLayoutInflater.inflate(layoutResID, mContentParent);方法,这里的layoutResId就是我们传进来的布局ID,然后将布局进行填充添加到界面中,这样我们的setContentView的整个工作就完成了。

    总结

    看一张示意图

    WX20190807-154652@2x.png

    我们以R.layout.screen_simple.xml为例来进行讲解,当调用setContentView的时候,系统会先对DecorView进行判断,如果为空的话就初始化,初始化完DecorView之后,在对其布局进行一个初始化,这个布局会根据开发者指定的样式来指定不同的布局,但是每一个布局文件中都会有一个id为contentFrameLayout控件,初始化完DevorView的布局的时候,也会初始化这个FrameLayout,源码中的字段就是mContentParent,在拿到这个mContentParent之后,会将我们传入的布局文件加载到这个FrameLayout中,这样我们就能看见自己写的布局文件了

    相关文章

      网友评论

        本文标题:setContentView是如何把布局加上去的

        本文链接:https://www.haomeiwen.com/subject/ruaodctx.html