疑问1:setContentView到底做了些什么,为什么调用后就可以显示出我们想要的布局页面?
疑问2:PhoneWindow倒是什么东西?Window和它是什么关系?
疑问3:DecorView是干什么用的?和我们的布局又有什么样的关系
疑问4:requestFeature为什么要在setContentView之前调用?
首先我们提出了以上几个疑问,在这里我一点需要说明,就是说大家在研读Android源码的时候一定要带着问题去读,否则就很容易走偏陷入源码无法自拔了。一个问题就是一条线索这样我们看源码得而效率会更高。
废话不多说,我们先来看一下第一个问题。
疑问1:setContentView方法我们并不陌生,但是又有多少人考虑过其背后的工作原理呢?我们随便新建一个Activity,继承Activity而不是AppCompatActivity调用setContentView方法,好了进入到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();
}
1,可以看到Activity的setContentView方法代码很简单,它调用了getWindow.setContentView并将xml文件作为参数继续向下传递,getWindow其实就是Window对象。也就是说xml文件被传递到了Window的setContentView方法中去做进一步的处理,点到Window类看一看;
2,点击Window类进去发现它是一个抽象类,PhoneWindow类是Window的唯一实现类。通过注释可以发现 Window类的作用:是显示最底层的窗口的外观,封装了一些行为,它的实现类必须添加到windowManager里面,它提供了一些UI相关处理 ,如background,title区域,key事件的处理而它的实现类是PhoneWindow
/**
* 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.
*
* The only existing implementation of this abstract class is
* android.policy.PhoneWindow, which you should instantiate when needing a
* Window. Eventually that class will be refactored and a factory method
* added for creating Window instances without knowing about a particular
* implementation.
*/
public abstract class Window {
代码省略
}
3,下面我们再看Window的实现类PhoneWindow,Activity的 getWindow().setContentView(layoutResID) ,此方法其实是调用的PhoneWindow的setContentView方法,在PhoneWindow类中setContentView方法重载有三个,我们只看一个就好;setContentView方法里面又两个关键变量,DecorView mDecor和ViewGroup mContentParent如下:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor; //解释:DecorView是Window的根节点,它是一个RrameLayout
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;//解释:mContentParent是Window内容要摆放的地方,要么是Decor要么是Decor的child
@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();
}
}
代码的解构很清晰,首先判断mContentParent是不是空如果是就调用installDecor(),如果当此Activity有“转场动画”的情况则调用mContentParent.removeAllViews()来清空所有的子view,注意mContentParent是DecorView的子View,最后调用mLayoutInflater.inflate(layoutResID,mContentParent)将我们定义的layout资源文件通过LayoutInflater对象转换为View树,并且添加至mContentParent视图中,写到这是不是觉得很熟悉?我们在Adapter不是经常这么干?所以说setContentView()方法的到最后还是要利用LayoutInflater来加载布局文件!
再来看下PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,如下:
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// 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)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View已,这里直接使用View的addView方法追加道了当前mContentParent而已。
所以说在我们的应用程序里可以多次调用setContentView()来显示界面,因为会removeAllViews,当然如果有Activity切换的过度动画时,TransitionManager会对它进行处理,可以把关注点放在大体流程上,不要太抠细节。
疑问2:疑问1中已经回答
疑问3:
DecorView是PhoneWindow类的一个内部类,继承了FrameLayout因此说DecorView是一个View不为过,该类是 FrameLayout子类,即一个ViewGroup视图。DecorView有且仅有一个子View就是mContentParent,那么DecorView对象是何时创建的呢?这个问题先不急我们继续看疑问4.
疑问4:
为什么requestFeature为什么要在setContentView之前调用?在上文当中我们提到了一个mContentParent,可以说我们的主题是什么样子就取决于这个mContentParent,为此我们展开探索在setContentView方法中调用了installDecor();我们看下这个方法代码
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
//根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
mContentParent = generateLayout(mDecor);
//......
//初始化一堆属性值
}
}
有两个地方是我们要注意的mDecor = generateDecor();和mContentParent = generateLayout(mDecor);首先判断mDecor对象是否为空,如果为空则调用generateDecor()创建一个DecorView(该类是FrameLayout子类,即一个ViewGroup视图),然后设置一些属性,generateDecor()方法是用来创建DecorView对象的也就是说到这我们的DecorView对象就算是真的创建出来了,我们看下PhoneWindow的generateDecor方法,如下:
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
generateDecor方法仅仅是new一个DecorView的实例。
回到installDecor方法继续往下看,第10行开始到方法结束都需要一个if (mContentParent == null)判断为真才会执行,当mContentParent对象不为空则调用generateLayout()方法去实例化mContentParent对象。generateLayout()方法也是我们加载主题的地方,所以我们看下generateLayout方法源码,如下:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//......
//依据主题style设置一堆值进行设置
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
//......
//根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值
//把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
//......
//继续一堆属性设置,完事返回contentParent
return contentParent;
}
可以看见上面方法主要作用就是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件。mDecor做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。
好了经过以上分析,我们得出结论Window的主题设置要经过setContentView()、installDecor()最后generateLayout(),那么现在我们就可以来回答疑问4了,当setContentView()方法之后主题已经被设定好了,我们再要想去修改主题就自然没有了效果。
所以我们平时写应用Activity时设置的theme或者feature吗(全屏啥的,NoTitle等)?我们一般是不是通过XML的android:theme属性或者java的requestFeature()方法来设置的呢?譬如:
通过java文件设置:
requestWindowFeature(Window.FEATURE_NO_TITLE);
通过xml文件设置:
android:theme="@android:style/Theme.NoTitleBar"
其实我们平时requestWindowFeature()设置的值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。
所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。
我们继续关注一下generateLayout方法的layoutResource变量赋值情况。当我们的Activity设置某个主题后,会给layoutResource赋值,其实就是一个xml布局,比如隐藏标题栏的布局等。因为它最终通过View in = mLayoutInflater.inflate(layoutResource, null);和decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));将in添加到PhoneWindow的mDecor对象。为例验证这一段代码分析我们用一个实例来进行说明,如下是一个简单的App主要代码:
AndroidManifest.xml文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yanbober.myapplication" >
<application
......
//看重点,我们将主题设置为NoTitleBar
android:theme="@android:style/Theme.Black.NoTitleBar" >
......
application>
manifest>
主界面布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:text="@string/hello_world"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
RelativeLayout>
APP运行界面:
上面我们将主题设置为NoTitleBar,所以我们找到在generateLayout方法中的layoutResource变量值为R.layout.screen_simple,所以我们看下系统这个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"
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>
布局中,一般会包含ActionBar,Title,和一个id为content的FrameLayout,这个布局是NoTitle的。
再来看下上面这个App的hierarchyviewer图谱,如下:
通过这个App的hierarchyviewer和系统screen_simple.xml文件比较就验证了上面我们分析的结论,不再做过多解释。
然后回过头可以看见上面PhoneWindow类的setContentView方法最后通过调运mLayoutInflater.inflate(layoutResID, mContentParent);或者mContentParent.addView(view, params);语句将我们的xml或者Java View插入到了mContentParent(id为content的FrameLayout对象)ViewGroup中。
最后setContentView还会调用一个Callback接口的成员函数onContentChanged来通知对应的Activity组件视图内容发生了变化。
Window类内部接口Callback的onContentChanged方法
上面刚刚说了PhoneWindow类的setContentView方法中最后调运了onContentChanged方法。我们这里看下setContentView这段代码,如下:
public void setContentView(int layoutResID) {
......
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
首先通过getCallback获取对象cb(回调接口),PhoneWindow没有重写Window的这个方法,所以到抽象类Window中可以看到:
/**
* Return the current Callback interface for this window.
*/
public final Callback getCallback() {
return mCallback;
}
这个mCallback在哪赋值的呢,继续看Window类发现有一个方法,如下:
public void setCallback(Callback callback) {
mCallback = callback;
}
Window中的mCallback是通过这个方法赋值的,那就回想一下,Window又是Activity的组合成员,那就是Activity一定调运这个方法了,回到Activity发现在Activity的attach方法中进行了设置,如下:
final void attach(Context context, ActivityThread aThread,
......
mWindow.setCallback(this);
......
}
也就是说Activity类实现了Window的Callback接口。那就是看下Activity实现的onContentChanged方法。如下:
public void onContentChanged() {
}
onContentChanged是个空方法。那就说明当Activity的布局改动时,即setContentView()或者addContentView()方法执行完毕时就会调用该方法。
所以当我们写App时,Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。
2-5 setContentView源码分析总结
可以看出来setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的过程可以重点概括为:
创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。
将Activity的布局文件添加至id为content的FrameLayout内。
至此整个setContentView的主要流程就分析完毕。你可能这时会疑惑,这么设置完一堆View关系后系统是怎么知道该显示了呢?下面我们就初探一下关于Activity的setContentView在onCreate中如何显示的(声明一下,这里有些会暂时直接给出结论,该系列文章后面会详细分析的)。
2-6 setContentView完以后Activity显示界面初探
这一小部分已经不属于sentContentView的分析范畴了,只是简单说明setContentView之后怎么被显示出来的(注意:Activity调运setContentView方法自身不会显示布局的)。
记得前面有一篇文章《Android异步消息处理机制详解及源码分析》的3-1-2小节说过,一个Activity的开始实际是ActivityThread的main方法(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。
那在这一篇我们再直接说一个知识点(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。
当启动Activity调运完ActivityThread的main方法之后,接着调用ActivityThread类performLaunchActivity来创建要启动的Activity组件,在创建Activity组件的过程中,还会为该Activity组件创建窗口对象和视图对象;接着Activity组件创建完成之后,通过调用ActivityThread类的handleResumeActivity将它激活。
所以我们先看下handleResumeActivity方法一个重点,如下:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
......
// TODO Push resumeArgs into the activity for consideration
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
......
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
......
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
......
// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
......
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
......
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
......
}
}
看见r.activity.makeVisible();语句没?调用Activity的makeVisible方法显示我们上面通过setContentView创建的mDecor视图族。所以我们看下Activity的makeVisible方法,如下:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
看见没有,通过DecorView(FrameLayout,也即View)的setVisibility方法将View设置为VISIBLE,至此显示出来。
大部分来自大神博客:
http://blog.csdn.net/yanbober/article/details/45970721
http://rkhcy.github.io/2017/05/16/setContentView%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/