Android走进Framework之AppCompatActivity.setContentView

来源:互联网 发布:sqoop mysql导入hbase 编辑:程序博客网 时间:2024/06/07 23:27

转载请标明出处: http://www.weyye.me/detail/framework-appcompatactivity-setcontentview/
本文出自:【Wey Ye的博客】

前言

在上一篇Android走进Framework之app是如何被启动的中讲到了从我们点击app一直到调用Activity.onCreate()的整个流程,今天来研究下我们最熟悉的一行代码setContentView()。网上也有很多关于setContentView的源码解析,但是都是基于Activity源码,而我们现在都是继承的AppCompatActivity,看源码发现改动还不少,所以我打算来研究下AppCompatActivity里是如何把我们的布局添加进去的。你是否也曾有过同样的疑惑,为什么创建Activity就要在onCreate()里面调用setContentView()?那就让我们来RTFSC (Read the fucking source code )。

学前疑惑

  • setContentView中到底做了什么?为什么我们调用后就可以显示到我们想到的布局?
  • PhoneWindow是个什么鬼?Window和它又有什么关系?
  • DecorView什么干嘛的?和我们的布局有什么联系?
  • 在我们调用requestFeature的时候为什么要在setContentView之前?

接下来,我们就来解决这些疑惑!以下源码基于Api24

AppCompatActivity.setContentView

我们先来看下AppCompatActivitysetContentView中做了什么
AppCompatActivity.java

    //这个是我们最常用的    @Override    public void setContentView(@LayoutRes int layoutResID) {        getDelegate().setContentView(layoutResID);    }    @Override    public void setContentView(View view) {        getDelegate().setContentView(view);    }    @Override    public void setContentView(View view, ViewGroup.LayoutParams params) {        getDelegate().setContentView(view, params);    }

可以看到3个重载的方法都调用getDelegate(),而其他的方法也都是调用了getDelegate(),很显然这个是代理模式。那么这个getDelegate()返回的是什么呢?

AppCompatActivity.java

    /**     * @return The {@link AppCompatDelegate} being used by this Activity.     */    @NonNull    public AppCompatDelegate getDelegate() {        if (mDelegate == null) {            //第一次为空,创建了AppCompatDelegate            mDelegate = AppCompatDelegate.create(this, this);        }        return mDelegate;    }

我们来看下AppCompatDelegate是怎么创建的

AppCompatDelegate.java

    private static AppCompatDelegate create(Context context, Window window,            AppCompatCallback callback) {        final int sdk = Build.VERSION.SDK_INT;        if (BuildCompat.isAtLeastN()) {            //7.0以及7.0以上创建AppCompatDelegateImplN            return new AppCompatDelegateImplN(context, window, callback);        } else if (sdk >= 23) {            //6.0创建AppCompatDelegateImplV23            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);        }    }

哦~原来根据不同的api版本返回不同的Delegate,我们先来看看AppCompatDelegateImplN,里面是否有setContentView

AppCompatDelegateImplN.java

@RequiresApi(24)@TargetApi(24)class AppCompatDelegateImplN extends AppCompatDelegateImplV23 {    AppCompatDelegateImplN(Context context, Window window, AppCompatCallback callback) {        super(context, window, callback);    }    @Override    Window.Callback wrapWindowCallback(Window.Callback callback) {        return new AppCompatWindowCallbackN(callback);    }    class AppCompatWindowCallbackN extends AppCompatWindowCallbackV23 {        AppCompatWindowCallbackN(Window.Callback callback) {            super(callback);        }        @Override        public void onProvideKeyboardShortcuts(                List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {            final PanelFeatureState panel = getPanelState(Window.FEATURE_OPTIONS_PANEL, true);            if (panel != null && panel.menu != null) {                // The menu provided is one created by PhoneWindow which we don't actually use.                // Instead we'll pass through our own...                super.onProvideKeyboardShortcuts(data, panel.menu, deviceId);            } else {                // If we don't have a menu, jump pass through the original instead                super.onProvideKeyboardShortcuts(data, menu, deviceId);            }        }    }}

发现并没有setContentView,那么肯定在父类。诶,它继承AppCompatDelegateImplV23,而AppCompatDelegateImplV23又继承AppCompatDelegateImplV14,AppCompatDelegateImplV14又继承AppCompatDelegateImplV11,AppCompatDelegateImplV11又继承AppCompatDelegateImplV9,好,知道关系后我有点懵逼了,搞什么鬼?客官别急,我们先来画个图

类继承关系
ok,最后我在V9里找到setContentView,我们来看下

AppCompatDelegateImplV9.java

    @Override    public void setContentView(int resId) {        //这个很关键,稍后会讲        ensureSubDecor();        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);        contentParent.removeAllViews();        //把我们的布局放到contentParent里面        LayoutInflater.from(mContext).inflate(resId, contentParent);        mOriginalWindowCallback.onContentChanged();    }    @Override    public void setContentView(View v) {        ensureSubDecor();        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);        contentParent.removeAllViews();        contentParent.addView(v);        mOriginalWindowCallback.onContentChanged();    }    @Override    public void setContentView(View v, ViewGroup.LayoutParams lp) {        ensureSubDecor();        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);        contentParent.removeAllViews();        contentParent.addView(v, lp);        mOriginalWindowCallback.onContentChanged();    }

这是对应的3个实现的方法,发现都会调用ensureSubDecor();并且都会找到contentParent,然后把我们的布局放入进去

ok,到这里我们来捋一捋流程。

大致流程

    private void ensureSubDecor() {        if (!mSubDecorInstalled) {            //这个mSubDecor其实就ViewGroup,调用createSubDecor()后,此时存放我们的布局的容器已经准备好了            mSubDecor = createSubDecor();//核心代码!            // If a title was set before we installed the decor, propagate it now            CharSequence title = getTitle();            if (!TextUtils.isEmpty(title)) {                onTitleChanged(title);            }            applyFixedSizeWindow();            //SubDecor 安装后的回调            onSubDecorInstalled(mSubDecor);            //设置标记位            mSubDecorInstalled = true;            // Invalidate if the panel menu hasn't been created before this.            // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu            // being called in the middle of onCreate or similar.            // A pending invalidation will typically be resolved before the posted message            // would run normally in order to satisfy instance state restoration.            PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);            if (!isDestroyed() && (st == null || st.menu == null)) {                invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);            }        }    }

调用了createSubDecor(),看字面意思创建了一个SubDecor,看似跟DecorView有联系。我们看下里面做了什么操作

    private ViewGroup createSubDecor() {        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {            a.recycle();            //还记得我们使用AppCompatActivity如果不设置AppCompat主题报的错误吗?就是在这里抛出来的            throw new IllegalStateException(                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");        }        //初始化相关特征标志        if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {            //一般我们的主题默认都是NoTitle            requestWindowFeature(Window.FEATURE_NO_TITLE);        } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {            // Don't allow an action bar if there is no title.            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();        //重点!在这里就创建DecorView,至于DecorView到底是什么以及如何创建的,稍后会讲到        mWindow.getDecorView();        final LayoutInflater inflater = LayoutInflater.from(mContext);        //可以看到其实就是个ViewGroup,我们接着往下看,跟DecorView到底有啥关系        ViewGroup subDecor = null;        if (!mWindowNoTitle) {            //上面说了主题默认都是NoTitle,所以不会走里面的方法            if (mIsFloating) {                // If we're floating, inflate the dialog title decor                subDecor = (ViewGroup) inflater.inflate(                        R.layout.abc_dialog_title_material, null);                ...            } else if (mHasActionBar) {                TypedValue outValue = new TypedValue();                mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);                ...                // Now inflate the view using the themed context and set it as the content view                subDecor = (ViewGroup) LayoutInflater.from(themedContext)                        .inflate(R.layout.abc_screen_toolbar, null);                /**                 * 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 {            //我们进入else            if (mOverlayActionMode) {                //调用了requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY)会走进来                subDecor = (ViewGroup) inflater.inflate(                        R.layout.abc_screen_simple_overlay_action_mode, null);            } else {                //ok,所以如果这些我们都没设置,默认就走到这里来了,在这里映射出了subDecor,稍后我们来看下这个布局是啥                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);        }        // Make the decor optionally fit system windows, like the window's decor        ViewUtils.makeOptionalFitsSystemWindows(subDecor);        //这个contentView很重要,是我们布局的父容器,你可以把它直接当成FrameLayout        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(                R.id.action_bar_activity_content);        //看过相关知识的同学应该知道android.R.id.content这个Id在以前是我们布局的父容器的Id        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);            }            //注意!原来windowContentView的Id是android.R.id.content,现在设置成NO_ID            windowContentView.setId(View.NO_ID);            //在之前这个id是我们的父容器,现在将contentView设置成android.R.id.content,那么可以初步判定,这个contentView将会是我的父容器            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);            }        }        // Now set the Window's content view with the decor        //注意!重要!将subDecor放入到了这个Window里面,这个Window是个抽象类,其实现类是PhoneWindow,稍后会讲到        mWindow.setContentView(subDecor);        ....        return subDecor;    }

看到了requestWindowFeature是不是很熟悉?还记得我们是怎么让Activity全屏的吗?

    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        requestWindowFeature(Window.FEATURE_NO_TITLE);        getWindow().setFlags(WindowManager.LayoutParams.FILL_PARENT        ,WindowManager.LayoutParams.FILL_PARENT);        setContentView(R.layout.activity_test);    }

而且这两行代码必须在setContentView()之前调用,知道为啥了吧?因为在这里就把Window的相关特征标志给初始化了,在setContentView()之后调用就不起作用了!

在代码里其他比较重要的地方已写了注释,我们来看下这个abc_screen_simple.xml的布局到底是什么样子的

abc_screen_simple.xml

<android.support.v7.internal.widget.FitWindowsLinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/action_bar_root"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    android:fitsSystemWindows="true">    <android.support.v7.internal.widget.ViewStubCompat        android:id="@+id/action_mode_bar_stub"        android:inflatedId="@+id/action_mode_bar"        android:layout="@layout/abc_action_mode_bar"        android:layout_width="match_parent"        android:layout_height="wrap_content" />    <include layout="@layout/abc_screen_content_include" /></android.support.v7.internal.widget.FitWindowsLinearLayout>

abc_screen_content_include.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">    <android.support.v7.internal.widget.ContentFrameLayout            android:id="@id/action_bar_activity_content"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:foregroundGravity="fill_horizontal|top"            android:foreground="?android:attr/windowContentOverlay" /></merge>

原来这个subDecor就是FitWindowsLinearLayout

看到这2个布局,我们先把这2个布局用图画出来。

布局结构
(图不在美,能懂就行~)

从AppCompatActivity到现在布局,在我的脑海里浮现出这样的的画面。。。

那这是不是我们app最终的布局呢?当然不是,因为我们还没讲到非常重要的两行代码

mWindow.getDecorView();
mWindow.setContentView(subDecor);

注释中说道Window是个抽象类,其实现类是PhoneWindow。那么我们先来看PhoneWindow的getDecorView做了什么

PhoneWindow.java

public class PhoneWindow extends Window implements MenuBuilder.Callback {    @Override    public final View getDecorView() {        if (mDecor == null || mForceDecorInstall) {            installDecor();        }        return mDecor;    }    private void installDecor() {        //mDecor是DecorView,第一次mDecor=null,所以调用generateDecor        if (mDecor == null) {            mDecor = generateDecor();               ...         }         //第一次mContentParent也等于null        if (mContentParent == null) {            //可以看到把DecorView传入进去了            mContentParent = generateLayout(mDecor);        }    }}

在generateDecor()做了什么?其实返回了一个DecorView对象。

    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是啥呢?

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {    ...}

哦~原来继承FrameLayout,起到了装饰的作用。

我们在来看看generateLayout()做了什么。

    protected ViewGroup generateLayout(DecorView decor) {        TypedArray a = getWindowStyle();        //设置一堆标志位...        ...        if (!mForcedStatusBarColor) {            //获取主题状态栏的颜色            mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);        }        if (!mForcedNavigationBarColor) {            //获取底部NavigationBar颜色            mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);        }        //获取主题一些资源       ...        // Inflate the window decor.        int layoutResource;        int features = getLocalFeatures();        // System.out.println("Features: 0x" + Integer.toHexString(features));        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {            ...我们设置不同的主题以及样式,会采用不同的布局文件...        } else {            //记住这个布局,之后我们会来验证下布局的结构            layoutResource = R.layout.screen_simple;            // System.out.println("Simple!");        }        //要开始更改mDecor啦~        mDecor.startChanging();        //注意,此时把screen_simple放到了DecorView里面        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);        //这里的ID_ANDROID_CONTENT就是R.id.content;        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);        if (contentParent == null) {            throw new RuntimeException("Window couldn't find content container view");        }        ...        //这里的getContainer()返回的是个Window类,也就是父Window,一般为空        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;    }

可以看到根据不同主题属性使用的不同的布局,然后返回了这个布局contentParent

我们来看看这个screen_simple.xml布局是什么样子的

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

咦,这个布局结构跟subDecor好相似啊。。

好了,到目前为止我们知道了,当我们调用mWindow.getDecorView();的时候里面创建DecorView,然后又根据不同主题属性添加不同布局放到DecorView下,然后找到这个布局的R.id.content,也就是mContentParent。ok,搞清楚mWindow.getDecorView();之后,我们在来看看mWindow.setContentView(subDecor);(注意:此时把subDecor传入进去)

    @Override    public void setContentView(View view) {        //调用下面的重载方法        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));    }    @Override    public void setContentView(View view, ViewGroup.LayoutParams params) {        //在mWindow.getDecorView()已经创建了mContentParent        if (mContentParent == null) {            installDecor();        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {            mContentParent.removeAllViews();        }        //是否有transitions动画。没有,进入else        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {            view.setLayoutParams(params);            final Scene newScene = new Scene(mContentParent, view);            transitionTo(newScene);        } else {            //重要!!将这个subDecor也就是FitWindowsLinearLayout添加到这个mContentParent里面了            //mContentParent是FrameLayout,在之前设置的View.NO_ID            mContentParent.addView(view, params);        }        mContentParent.requestApplyInsets();        final Callback cb = getCallback();        if (cb != null && !isDestroyed()) {            cb.onContentChanged();        }        mContentParentExplicitlySet = true;    }

当调用了mWindow.getDecorView();创建了DecorView以及mContentParent,并且把subDecor放到了mContentParent里面。我们再来回头看看AppCompatDelegateImplV9,还记得它吗?当我们在AppCompatActivitysetContentView的时候会去调用AppCompatDelegateImplV9setContentView

AppCompatDelegateImplV9.java

@Override    public void setContentView(View v) {        //此时DecorView和subDecor都创建好了        ensureSubDecor();        //还记得调用createSubDecor的时候把原本是R.id.content的windowContentView设置成了NO_ID,并且将contentView也就是ContentFrameLayout设置成了R.id.content吗?也就是说此时的contentParent就是ContentFrameLayout        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);        contentParent.removeAllViews();        //将我的布局放到contentParent里面        contentParent.addView(v);        mOriginalWindowCallback.onContentChanged();    }    @Override    public void setContentView(int resId) {        ensureSubDecor();        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);        contentParent.removeAllViews();        //将我们的布局id映射成View并且放到contentParent下        LayoutInflater.from(mContext).inflate(resId, contentParent);        mOriginalWindowCallback.onContentChanged();    }    @Override    public void setContentView(View v, ViewGroup.LayoutParams lp) {        ensureSubDecor();        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);        contentParent.removeAllViews();        contentParent.addView(v, lp);        mOriginalWindowCallback.onContentChanged();    }

完整布局

ok,看到这里,想必大家在脑海里也有个大致布局了吧,我们再来把整个app初始布局画出来

不喜勿喷...

验证布局

接下来我们来验证下我们布局结构是否正确

新建一个Activity

    public class TestAcitivty extends AppCompatActivity {    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_test);    }}

主题

<!-- Base application theme. -->    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">        <!-- Customize your theme here. -->        <item name="colorPrimary">@color/colorPrimary</item>        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>        <item name="colorAccent">@color/colorAccent</item>        <item name="android:listDivider">@color/divider_dddddd</item>    </style>

为了演示布局非常简单,就是一个textview

<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android="http://schemas.android.com/apk/res/android"          android:id="@+id/textView"          android:layout_width="match_parent"          android:layout_height="match_parent"          android:orientation="vertical"></TextView>

运行后,我们在用hierarchyviewer查看下

这里写图片描述

看来我们的脑补的布局是对的!

学后总结

整个流程就是这样。看到这里我们明白了,当我们调用setContentView的时候加载了2次系统布局,在PhoneWindow里面创建了DecorView,DecorView是我们的最底层的View,并且将我们的布局放入到一个ContentFrameLayout里,我们还知道在setContentView的时候进行了相关特征标志初始化,所以在它之后调用requestWindowFeature就会不起作用然后报错。

setContentView时序图

知道这些之后我们不妨用时序图来梳理下整个调用的流程

setContentView时序图

查看高清无码大图

致谢

【凯子哥带你学Framework】Activity界面显示全解析(上)

结语

当然,在这篇文章中,因为篇幅问题,也有许多没有讲的重要知识点,比如:

  • PhoneWindow在哪里初始化?它做了哪些事?
  • view树是如何被管理的?
  • findViewById到底是怎么找到对应的View的?
  • 为什么说setContentViewonResume在对用户可见?
  • 等等…

在下一篇中我会详细讲解这些问题。

最后

文中如有错误,希望大家指出!

博主整理不易,转载请注明出处:
http://www.weyye.me/detail/framework-appcompatactivity-setcontentview/

0 0