深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?

深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?_第1张图片

UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识如何优化 UI 渲染两部内容。


UI 优化系列专题
  • UI 渲染背景知识

《View 绘制流程之 setContentView() 到底做了什么?》
《View 绘制流程之 DecorView 添加至窗口的过程》
《深入 Activity 三部曲(3)View 绘制流程》
《Android 之 LayoutInflater 全面解析》
《关于渲染,你需要了解什么?》
《Android 之 Choreographer 详细分析》

  • 如何优化 UI 渲染

《Android 之如何优化 UI 渲染(上)》
《Android 之如何优化 UI 渲染(下)》


setContentView() 相信大家肯定不会感到陌生,几乎每个 Activity 都会使用该方法为其添加一个 xml 布局界面。但是你真的了解 setContentView 方法吗?为什么通过它就可以展示出我们添加的 xml 布局界面呢?

关于 Activity 的 View 加载过程大家肯定听说过 Window、PhoneWindow、DecorView 等内容,它们之间是什么关系?我们先通过几个问题来了解下。

相关问题
  1. setContentView 方法到底做了什么?为什么调用后可以显示我们设置的布局?
  • PhoneWindow 是什么?它和 Window 是什么关系?
  • DecorView 是干什么用的?和我们添加的布局又有什么关系?
  • requestFeatrue 方法为什么要在 setContentView 方法之前?
  1. Layoutinflater 到底怎么把 XML 布局文件添加到 DecorView 上?
  • 标签为什么不能作为布局的根节点?
  • 标签为什么要作为布局资源的根节点?
  • inflate( int resource, ViewGroup root, boolean attachToRoot) 参数 root 和 attachToRoot 的作用和规则?
  1. AppComatActivity 实现原理是怎样的?它是如何完成布局兼容的?

如果以上问题你都能够熟练并正确的回答出来,那么恭喜你可以直接跳过该篇文章了。


1. 从 setContentView 开始

打开 Activity 源码找到 setContentView 方法如下:

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

getWindow 方法返回一个 Window 对象:

public Window getWindow(){
    return mWindow;
}

但是 Window 本质上是一个抽象类,在 Window 的源码中对其介绍是这样的:

Window 是一个显示顶层窗口的外观,包括一些基础行为的封装(如 findViewById()、事件分发 dispatch 等),而且每一个 Window 实例必须添加到 WindowManager 里面,它提供了标准的 UI 策略,比如背景、标题区域等。它的唯一实现类是 PhoneWindow

此时我们需要去跟踪下 Window 的创建过程,翻阅 Activity 源码发现在它的 attach 方法:

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
     //content实际类型是ContextImpl
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);
     //可以看到Window的类型是PhoneWindow
     //创建当前 Activity 的 Window 实例。
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);

    //... 省略

}

当启动一个 Activity 时首先创建该 Activity 实例,随后就会调用它的 attach 方法(这部分内容主要在 ActivityThread performLaunchActivity())。在 attach 方法中我们可以看到 Window 的实际类型是 PhoneWindow。

也就是当我们通过 Activity 的 setContentView 实际是调用了 PhoneWindow 的 setContentView 方法:

public void setContentView(int layoutResID) {
    //mContentParent是ViewGroup
    //我们的setContentView设置的布局实际就是被添加到该容器中 
    //它是我们添加布局文件的直接根视图
    if (mContentParent == null) {
        //mContentParent默认为null
        //安装当前DecorView
        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 {
        //解析布局资源,添加到mContentParent
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        //回调Activity的onContentChanged方法
        cb.onContentChanged();
    }
    //表示已经设置过布局
    //如果此时使用requestFeature则会抛出异常
    mContentParentExplicitlySet = true;
}
  • mContentParent 是一个 ViewGroup,它的实际类型是 FrameLayout,实际我们通过 setContentView 设置的 View 就被添加到该容器。也就是它是我们布局文件的直接父视图(下面会分析到)。

  • 方法最后 mContentParentExplicitlySet,在 setContentView 方法执行完毕后置为 true,表示当前窗口已经设置完成。后面我们会分析道,如果此后在调用 requestFeature 方法设置 Window 窗口的 Feature 将会抛出异常。

mContentParent 默认为 null,此时执行 installDecor 方法,为当前 Window 创建 DecorView 视图,DecorView 是我们整个 Activity 的最顶级视图,它的实际类型是 FrameLayout:

private void installDecor() {
    mForceDecorInstall = false;
    //mDecor是DecorView,继承自FrameLayout
    if (mDecor == null) {
        //为当前Window创建DecorView
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    //mContentParent默认为null
    if (mContentParent == null) {
        //找到当前主题布局中,内容的父容器
        mContentParent = generateLayout(mDecor);

        // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
        mDecor.makeOptionalFitsSystemWindows();

      //... 省略
}

每个 Window 有且仅有一个 DecorView,DecorView 用来描述窗口的视图,看下它的创建过程 generateDecor 方法如下:

protected DecorView generateDecor(int featureId) {
    Context context;
    //mUseDecorContext构造方法中默认置为true

    //表示使用DecorContext山下文
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            //使用DecorContext
            context = new DecorContext(applicationContext, getContext().getResources());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        //使用应用程序上下文
        context = getContext();
    }
    //直接创建DecorView
    return new DecorView(context, featureId, this, getAttributes());
}

方法的最后可以看到直接创建 DecorView 并返回(DecorView 继承自 FrameLayout)。回到 installDecor 方法,看下 setContentView 方法的直接父容器 mContentParent 的创建过程 generateLayout 方法(注意这时候 mContentParent 与 DecorView 还没有任何关联):

//我们给Window设置的相关属性就是在generateLayout时加进来的
protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    //获取当前Window的Style
    //这个是不是很熟悉,
    TypedArray a = getWindowStyle();

    //Window是否是Floating
    //浮窗类型时 Dialog 就是Floating 类型
    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);
    }

    //是否需要标题栏
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        //对Feature状态为进行设置
        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);
    }

    if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
        requestFeature(FEATURE_ACTION_BAR_OVERLAY);
    }

    if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
        requestFeature(FEATURE_ACTION_MODE_OVERLAY);
    }

    if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
        requestFeature(FEATURE_SWIPE_TO_DISMISS);
    }

    if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
        setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
            false)) {
        setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
                & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation,
            false)) {
        setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION
                & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowOverscan, false)) {
        setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
        setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER & (~getForcedWindowFlags()));
    }

    if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch,
            getContext().getApplicationInfo().targetSdkVersion
                    >= android.os.Build.VERSION_CODES.HONEYCOMB)) {
        setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH & (~getForcedWindowFlags()));
    }

    a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
    a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
    if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) {
        if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedWidthMajor,
                mFixedWidthMajor);
    }
    if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {
        if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedWidthMinor,
                mFixedWidthMinor);
    }
    if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) {
        if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedHeightMajor,
                mFixedHeightMajor);
    }
    if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) {
        if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
        a.getValue(R.styleable.Window_windowFixedHeightMinor,
                mFixedHeightMinor);
    }
    if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
        requestFeature(FEATURE_CONTENT_TRANSITIONS);
    }
    if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
        requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
    }

    //这个地方是不是很熟悉,Window是否是透明的
    mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);

    /**以上都是对Window一些状态进行设置*/
    //requestFeature为什么要在setContentView之前?

    final Context context = getContext();
    final int targetSdk = context.getApplicationInfo().targetSdkVersion;
    final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
    final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
    final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
    final boolean targetHcNeedsOptions = context.getResources().getBoolean(
            R.bool.target_honeycomb_needs_options_menu);
    final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);

    if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
        setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);
    } else {
        setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);
    }

    if (!mForcedStatusBarColor) {
        mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
    }
    if (!mForcedNavigationBarColor) {
        mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
    }

    WindowManager.LayoutParams params = getAttributes();

    // Non-floating windows on high end devices must put up decor beneath the system bars and
    // therefore must know about visibility changes of those.
    if (!mIsFloating && ActivityManager.isHighEndGfx()) {
        if (!targetPreL && a.getBoolean(
                R.styleable.Window_windowDrawsSystemBarBackgrounds,
                false)) {
            setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                    FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags());
        }
        if (mDecor.mForceWindowDrawsStatusBarBackground) {
            params.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
        }
    }
    if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
        decor.setSystemUiVisibility(
                decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
    }

    if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
            >= android.os.Build.VERSION_CODES.HONEYCOMB) {
        if (a.getBoolean(
                R.styleable.Window_windowCloseOnTouchOutside,
                false)) {
            setCloseOnTouchOutsideIfNotSet(true);
        }
    }

    if (!hasSoftInputMode()) {
        params.softInputMode = a.getInt(
                R.styleable.Window_windowSoftInputMode,
                params.softInputMode);
    }

    if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
            mIsFloating)) {
        /* All dialogs should have the window dimmed */
        if ((getForcedWindowFlags() & WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
            params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        }
        if (!haveDimAmount()) {
            params.dimAmount = a.getFloat(
                    android.R.styleable.Window_backgroundDimAmount, 0.5f);
        }
    }

    if (params.windowAnimations == 0) {
        params.windowAnimations = a.getResourceId(
                R.styleable.Window_windowAnimationStyle, 0);
    }

    // The rest are only done if this window is not embedded; otherwise,
    // the values are inherited from our container.
    if (getContainer() == null) {
        if (mBackgroundDrawable == null) {
            if (mBackgroundResource == 0) {
                mBackgroundResource = a.getResourceId(
                        R.styleable.Window_windowBackground, 0);
            }
            if (mFrameResource == 0) {
                mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
            }
            mBackgroundFallbackResource = a.getResourceId(
                    R.styleable.Window_windowBackgroundFallback, 0);
        }
        if (mLoadElevation) {
            mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
        }
        mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
        mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
    }

    // Inflate the window decor.

    /**生成对应的Window Decor 要根据当前设置的Features属性
     * 加载不同的 DecorView 的xml布局*/
    int layoutResource;
    //获取当前Window的Features
    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;
        setCloseOnSwipeEnabled(true);
    } 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 {
        // 这是最简单的一个,看下 DecorView 要加载布局文件是怎样的?
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    mDecor.startChanging();
    //将DecorView的xml文件添加到DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    //在DecorView对应的布局中,查找id为content的FrameLayout,该容器便是我们布局直接父容器
    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(contentParent);
    }


    //... 省略

    mDecor.finishChanging();

    //返回DecorView对应布局中的id为content的FrameLayout
    //它实际上就是我们setContentView的直接根视图
    return contentParent;
}

generateLayout 方法虽然较长,但是工作内容并不复杂,我们首先看 getWindowStyle 方法:

public final TypedArray getWindowStyle() {
    synchronized (this) {
        if (mWindowStyle == null) {
            //styleable是不是很熟悉,在一些自定义控件时经常用到
            mWindowStyle = mContext.obtainStyledAttributes(
                    com.android.internal.R.styleable.Window);
        }
        return mWindowStyle;
    }
}

styleable 是不是很熟悉,在一些自定义控件时经常会用到。实际上我们给 Window 设置的相关属性就是在 generateLayout 方法进行设置的,例如非常熟悉和经常使用到的 :

//Window是否是Floating状态
//Dialog时Window就是Floating
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);

//Window是否包含标题栏
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {

//Window是否是透明的
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);

然后它们都会调用 requestFeature 方法对当前 Window 的 Feature 状态位进行设置。

public boolean requestFeature(int featureId) {
    if (mContentParentExplicitlySet) {
        //在setContentView方法最后会将该标志位置为true,如果
        //在setCotnentView方法后再执行requestFeature将会抛出异常。
        throw new AndroidRuntimeException("requestFeature() must be called before adding content");
    }
    final int features = getFeatures();
    final int newFeatures = features | (1 << featureId);
    if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
            (newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
        //不能既有自定义标题栏,又有其他标题栏
        throw new AndroidRuntimeException(
                "You cannot combine custom titles with other title features");
    }
    if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) {
        return false; // Ignore. No title dominates.
    }
    if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) {
        //没有标题栏
        removeFeature(FEATURE_ACTION_BAR);
    }
  //... 省略
}

注意方法中 if (mContentParentExplicitlySet) 如果满足则直接抛出异常。该标志位在上面也有分析到, setContentView 方法最后会将其置为 true。即 requestFeature 方法必须在 setContentView 方法之前。那为什么要在 setContentView 方法之前呢?下面分析到。

要根据当前 Feature 加载不同的 DecorView 的 XML 布局文件。注意查看源码中 generateLayout 方法的下半部分,我们以最简单的 R.layout.screen_simple 布局为例:


  
  

可以看到布局本身就是一个 LinearLayout,包含上下两部:标题栏 ViewStub 区域,内容区域 id 为 content 的 FrameLayout(这就是 Window 中的 mContentParent,即 setContentView 的直接父容器)。

然后将DecorView 对应的 xml 布局文件添加到 DecorView 中:

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

DecorView 的 onResourcesLoaded 方法如下:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    mStackId = getStackId();
   //判断当前DecorView是否包含DecorCaptionView
    mDecorCaptionView = createDecorCaptionView(inflater);
    //通过LayoutInflater完成xml布局加载
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            //DecorCaptionView也是DecorView子视图
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
         //此时直接添加到DecorCaptionView中
        mDecorCaptionView.addView(root,
                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {
        // 添加到DecorView中
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

也就是说,Window 会根据当前设置的 Feature 为 DecorView 添加一个对应的 xml 布局文件,该布局文件主要划分上下两部分,其中包含一个 id 为 content 的 FrameLayout,它会赋值给 PhoneWindow 中的 mContentParent,表示 setContentView 的的父容器

在 DecorView 中找到对应的 mContentParent(就是 id 为 content 的 FrameLayout):

ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);

在 generateLayout 方法最后返回该 contentParent,此时赋值给 PhoneWidow 的成员 mContentParent。

重新回到 PhoneWindow 的 setContentView 方法。将我们这是的布局文件添加 contentParent 过程如下:

// 解析布局资源,添加到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);

至此,我们可以回答文章开头的第一个问题了,先通过一张图了解下 Activity 加载 UI - 类图关系和视图结构。

深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?_第2张图片
Activity加载UI-类图关系和视图结构
  • 每个 Activity 都有一个关联的 Window 对象(该对象实际类型为 PhoneWindow,PhoneWindow 为 Window 的唯一实现类)用来描述应用程序窗口。

  • 每个窗口内部又包含了一个 DecorView 对象,DecorView 继承自 FrameLayout;DecorView 用来描述窗口的视图 — xml 布局(我们通过 setContentView 方法设置的 xml 布局最终被添加到该 DecorView 对应 xml 布局中 id 为 content 的 FrameLayout 中,下面分析到)。

  • 另外 requestFeature 必须要在 setContentView 方法之前,因为要根据该 Feature 为 DecorView 添加一个对应的 xml 布局文件;该布局包含上下两部分,标题栏和内容区域(id 为 content 的 FrameLayout)。

2. LayoutInfalter 解析过程

需要回到 setContentView 方法,看下我们设置的布局是如何添加到 mContentParent 中:

//layoutResID通过setContentView设置的布局资源id
//mContentParent就是id为content的FrameLayout  
mLayoutInflater.inflate(layoutResID, mContentParent);

关于 LayoutInflater 大家肯定不会感到陌生,它可以将我们传入的 xml 布局文件解析成对应的 View 对象。

/**
 * resource 表示当前布局资源id
 * root 表示布局的父容器,可以为null
 * attachToRoot 是否将布局资源添加到父容器root上
 * */
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        //重点看下inflate方法
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

首先通过 Resources 获取一个 XML 资源解析器,我们重点关注 inflate 方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

        final Context inflaterContext = mContext;
        //获取在XML设置的属性
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        //注意root容器在这里,在我们当前分析中该root就是mContentParent
        View result = root;

        try {
            // 查找xml布局的根节点
            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();

            //判断是否是merge标签
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    //此时如果ViewGroup==null,与attachToRoot==false将会抛出异常
                    //merge必须添加到ViewGroup中,这也是merge为什么要作为布局的根节点,它要添加到上层容器中
                    throw new InflateException(" can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 否则创建该节点View对象
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                //如果contentParent不为null,在分析setContentView中,这里不为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);
                    }
                }

                //解析Child
                rInflateChildren(parser, temp, attrs, true);

                if (root != null && attachToRoot) {
                    //添加到ViewGroup
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    //此时布局根节点为temp
                    result = temp;
                }
            }

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

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        return result;
    }
}

while 循环部分,找到 xml 布局文件的根节点,如果 if (type != XmlPullParser.START_TAG) 未找到根节点直接抛异常了。否则获取到该节点名称,判断如果是 merge 标签,此时需要注意参数 root 和 attachToRoot,root 必须不为null,并且 attachToRoot 必须为 true,即 merge 内容必须要添加到 root 容器中

如果不是 merge 标签,此时根据标签名 name 直接创建该 View 对象,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法实际也是调用到了 rInflate 方法:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    //还是调用rInflate方法
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

区别在于最后一个参数 finishInflate,它的作用是标志当前 ViewGroup 树创建完成后回调其 onFinishInflate 方法。

如果根标签是 merge 此时 finishInflate 为 false,这也很容易理解,此时的父容器为 inflate 中传入的 ViewGroup,它是不需要再次回调 onFinishInflate() 的。该过程如下:

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;

    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)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            //include标签
            if (parser.getDepth() == 0) {
                //include如果为根节点则抛出异常了
                //include不能作为布局文件的根节点
                throw new InflateException(" cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            //如果此时包含merge标签,此时也会抛出异常
            //merge只能作为布局文件的根节点
            throw new InflateException(" must be the root element");
        } else {
            //创建该节点的View对象
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;

            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            //添加到父容器
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        //回调ViewGroup的onFinishInflate方法
        parent.onFinishInflate();
    }
}

while 循环部分,parser.next() 获取下一个节点,如果获取到节点名为 include,此时 parse.getDepth() == 0 表示根节点,直接抛出异常, 不能作为布局的根节点

如果此时获取到节点名称为 merge,也是直接抛出异常了, 只能作为布局的根节点

否则创建该节点对应 View 对象,rInflateChildren 递归完成以上步骤。并将解析到的 View 添加到其直接父容器 viewGroup.addView()。

注意方法的最后通知调用每个 ViewGroup 的 onFinishInflate(),大家是否有注意到这其实是入栈的操作,即最顶层的 ViewGroup 最后回调 onFinishInflate()。

至此,我们可以回答文章开头提出的第二个问题了,再来通过一张流程图熟悉下整个解析过程:

深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?_第3张图片
LayoutInflater 解析过程

在 inflater 解析布局资源过程中,首先找到布局的根节点 START_TAG,如果未找到直接抛出异常。否则获取到当前节点的名称。

  • 如果节点名称为 merge ,会判断 inflate 方法参数 if ( root(ViewGroup)!= null && attachToot == true ),表示布局文件要直接添加到 root 中,否则抛出异常( can be used only with a valid ViewGroup root and attachToRoot=true);

  • 继续解析子节点的过程中如果再次解析到 merge 标签,则直接抛出异常, 标签必须作为布局的根节点( must be the root element)。

  • 如果解析到节点名称为 include,会判断当前节点深度是否为 0,0 表示当前处于根节点,此时直接抛出异常,即 不能作为布局文件的根节点( cannot be the root element)。

3. 偷梁换柱之为兼容而生的 AppCompatActivity

在 Android Level 21 之后,Android 引入了 Material Design 的设计,为了支持 Material Color、调色版、Toolbar 等各种新特性,AppCompatActivity 就应用而生。Google 考虑到仍然有很大部分低于 5.0 版本的设备,所有将 AppCompatActivity 放在了 support v7 包内。

接下来我们就看下 AppCompatActivity 是如何实现 UI 兼容设计的。

//AppCompatActivity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

与 Activity 有所不同,AppCompatActivity 的 setContentView 方法中首先调用 getDelegate 方法得到一个代理对象。

public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        //创建AppCompatDelegate对象
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

创建当前 AppCompatDelegate 过程如下:

//AppCompatDelegate的create方法
private static AppCompatDelegate create(Context context, Window window,
        AppCompatCallback callback) {
    final int sdk = Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (sdk >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else if (sdk >= 14) {
        return new AppCompatDelegateImplV14(context, window, callback);
    } else if (sdk >= 11) {
        return new AppCompatDelegateImplV11(context, window, callback);
    } else {
        return new AppCompatDelegateImplV9(context, window, callback);
    }
}

根据当前系统版本创建对应的 AppCompatDelegate 对象,AppCompatActivity 其实就是通过引入 AppCompatDelegate 来解决兼容问题。

这里需要说明的是,各 Delegate 实际根据版本由高到低继承关系,即 AppCompatDelegateImplN extends AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 extends AppCompatDelegateImplV9。

setContentView 实际调用到 AppCompatDelegate 的第一个实现类 AppCompatDelegateImplV9 中:

public void setContentView(int resId) {
    //创建一个SubDecor
    ensureSubDecor();
   //获取SubDecor中content区域,此时setContentView的直接父容器为SubDecor中id为content的FrameLayout
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    //将布局添加到SubDecor的Content区域
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    //回调到Activity的onContentChanged,
    mOriginalWindowCallback.onContentChanged();
}

ensureSubDecor 方法是要创建一个 SubDecor,SubDecor 实际与 PhoneWindow 中的 DecorView 类似,它的出现就是为了兼容布局。

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        //创建SubDecor,
        mSubDecor = createSubDecor();

        //是否设置了标题
        CharSequence title = getTitle();
        if (!TextUtils.isEmpty(title)) {
            onTitleChanged(title);
        }

        applyFixedSizeWindow();
        onSubDecorInstalled(mSubDecor);
        //表示当前Window已经安装SubDecor
        mSubDecorInstalled = true;
        PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
        if (!isDestroyed() && (st == null || st.menu == null)) {
            invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
        }
    }
}

这里主要看下 SubDecor 的创建过程 createSubDecor 方法,该方法过程与 PhoneWindow 创建 DecorView 类似,区别是 Delegate 中找的都是 AppCompat 的属性,也就是做的兼容相关的事情。

private ViewGroup createSubDecor() {
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

    if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
        a.recycle();
        throw new IllegalStateException(
                "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
    }

    if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
        //调用到Window中requestFeature
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
    }
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
    }
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
        requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
    }
    mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
    a.recycle();

    /**
      * 以上根据style设置Window的Feature
      * createSubDecor 方法的上半部分
      */

    //确保Window中已经安装DecorView
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;


    //根据style加载SubDecor的xml布局
    if (!mWindowNoTitle) {
        //不需要标题栏类型窗口
        if (mIsFloating) {
            //Floating 类型窗口
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_dialog_title_material, null);

            // Floating windows can never have an action bar, reset the flags
            mHasActionBar = mOverlayActionBar = false;
        } else if (mHasActionBar) {
            //含有 ActionBar
            TypedValue outValue = new TypedValue();
            mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);

            Context themedContext;
            if (outValue.resourceId != 0) {
                themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
            } else {
                themedContext = mContext;
            }

            //解析SubDecor对应的xml布局文件
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);
           //同样包含一个id为content
            mDecorContentParent = (DecorContentParent) subDecor
                    .findViewById(R.id.decor_content_parent);
            mDecorContentParent.setWindowCallback(getWindowCallback());

            /**
             * Propagate features to DecorContentParent
             */
            if (mOverlayActionBar) {
                mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
            }
            if (mFeatureProgress) {
                mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
            }
            if (mFeatureIndeterminateProgress) {
                mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
            }
        }
    } else {
        if (mOverlayActionMode) {
            //解析DecorView对应xml布局文件
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_screen_simple_overlay_action_mode, null);
        } else {
            //解析DecorView对应xml布局文件
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        }

    //... 省略

    }

    if (subDecor == null) {
        throw new IllegalArgumentException(
                "AppCompat does not support the current theme features: { "
                        + "windowActionBar: " + mHasActionBar
                        + ", windowActionBarOverlay: "+ mOverlayActionBar
                        + ", android:windowIsFloating: " + mIsFloating
                        + ", windowActionModeOverlay: " + mOverlayActionMode
                        + ", windowNoTitle: " + mWindowNoTitle
                        + " }");
    }

    if (mDecorContentParent == null) {
        mTitleView = (TextView) subDecor.findViewById(R.id.title);
    }

    /**
      * 以下为 createSubDecor 方法的下半部分
      * 偷梁换柱过程,将 SubDecor 对应布局中 content 替换原 DecorView 中 id 为 content 的 FrameLayout。
      */
    // Make the decor optionally fit system windows, like the window's decor
    ViewUtils.makeOptionalFitsSystemWindows(subDecor);

    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        // There might be Views already added to the Window's content view so we need to
        // migrate them to our content view
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }

        //这里很关键,将原来Window中id为content的FrameLayout(mContentParent)设置为NO_ID
        windowContentView.setId(View.NO_ID);
        //最新创建的SubDecor中内容区域FramLayout的id设置为content
        //偷梁换柱
        contentView.setId(android.R.id.content);

        // The decorContent may have a foreground drawable set (windowContentOverlay).
        // Remove this as we handle it ourselves
        if (windowContentView instanceof FrameLayout) {
            ((FrameLayout) windowContentView).setForeground(null);
        }
    }

    // 将新创建的SubDecor添加到DecorView的内容区域(mContentParent容器)
    mWindow.setContentView(subDecor);

    //... 省略

    return subDecor;
}

可以看到方法的上半部分,获取一系列 AppCompat 兼容属性,设置 Window 的 Feature 属性;然后方法的中间部分,注意 mWindow.getDecorView() 作用是创建当前 Window 的 DecorView 整个过程(文章上面已经做了分析);然后根据 Feature 加载 SubDecor 对应的 xml 布局文件,这里我们以最简单的 abc_screen_simple.xml 布局文件为例:



  

  //这里包含一个FrameLayout
  


abc_screen_simple.xml 布局文件中 include 一个 abc_screen_content_include.xml 布局文件,如下:



    


ContentFrameLayout 继承自 FrameLayout,注意该 FrameLayout 最后会替代原 PhoneWindow 中 DecorView 对应布局内 id 为 content 的 FrameLayout。这一过程也是 AppCompatActivity 中偷梁换柱的核心内容,一起来看下这个重要过程,注意查看 createSubDecor 方法的下半部分:

获取到当前 SubDecor 中 content 容器,也就是上面 include 布局内 ContentFrameLayout。

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
        R.id.action_bar_activity_content);

然后拿到 PhoneWindow 中 DecorView 内 content 容器(mContentParent),注意这个原本是我们 setContentView() 的直接父容器。

final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

重要的偷梁换柱过程:

    // 这里很关键,将原来Window中id为content的FrameLayout(mContentParent)设置为NO_ID
    windowContentView.setId(View.NO_ID);
    // 最新创建的SubDecor中内容区域ContentFramLayout的id设置为content
    // 偷梁换柱
    contentView.setId(android.R.id.content);

将 DecorView 中原 content 容器 id 置为 NO_ID,将 SubDecor 中 content 容器 id 置为 content。经过前面的分析我们知道通过 setContentView 添加布局最终会被添加到一个 id 为 content 的 FrameLayout,此时该 content 实际变为 SubDecor 中 ContentFrameLayout容器。偷梁换柱完成。

然后将 SubDecor 添加到 DecorView 中,此时原 DecorView 中 content 容器实际添加的是 SubDecor。

// 将新创建的SubDecor添加到DecorView的内容区域(mContentParent容器)
mWindow.setContentView(subDecor);

最后我们重新回到 Delegate 的 setContentView 方法,看下我们设置的布局如何添加到 SubDecor 中的 content (ContentFrameLayout)容器的:

// 将布局添加到SubDecor的Content区域
LayoutInflater.from(mContext).inflate(resId, contentParent)

通过一张结构图了解下 AppCompatActivity 整个 UI 视图关系,注意与前面分析的 Activity 做下比较,最主要的差别在 DecorView 的 content 容器。此时已经替换成了 SubDecor。

深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?_第4张图片
AppCompatActivity 的 View 布局结构

至此,我们可以回答文章开头的第三个问题了。

  • AppCompatActivity 通过引入 AppCompatDelegate 来兼容不同版本的 Material Design 支持。

  • 在 AppCompatDelegate 中做了一个巧妙的偷梁换柱操作,即在原 DecorView 的 content 区域添加一个 SubDecor(兼容布局),我们通过 setContentView 设置的布局最终被添加到该 SubDecor 的 content 容器中,这样完成布局兼容操作。

其实我们可以看出 Google 工程师在处理兼容时也很“暴力”,这也是没有办法的办法,因为之前挖的坑太多了,这么多版本,为了做兼容费了很多心思,有些心思设计的也非常巧妙。

总结

每个 Activity 都有一个关联的 Window 对象,用来描述应用程序窗口,每个窗口内部又包含一个 DecorView 对象,DecorView 对象用来描述窗口的视图 — xml 布局。

AppCompatActivity 在 DecorView 中又添加了一个 SubDecor 视图 — xml 布局,解决布局兼容性问题。


以上便是个人在学习 View 的加载过程心得和体会,文中如有不妥或有更好的分析结果,欢迎大家指出。

文章如果对你有帮助,请留个赞吧!

你可能感兴趣的:(深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?)