平时我们加载xml文件都是直接在onCreate方法中调用setContentView,在加载Activity的onCreate方法时布局就被加载出来了,但是深入一点看,发现内容还是很多的,看了很多大神相关的博客,也写了个总结,内容可能不全,不足之处,还请多指教。
1.先从setContentView开始说起
Activity中有3个setContentView()
方法,可以看到, 这三个方法都是先getWindow
,获得一个Window对象,然后调用它的setContentView
,那么这个Window又是什么呢?
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
///
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
////
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
1.1 创建Window对象
getWindow返回的是mWindow,而这个mWindow是由PolicyManager创建的,PolicyManager提供了静态类方法(这里用到了工厂模式,PolicyManager提供工厂方法),创建了一个PhoneWindow 对象。
//创建一个Window对象
mWindow = PolicyManager.makeNewWindow(this);
最终创建Window对象的方法
//创建具体对象的接口
public PhoneWindow makeNewWindow(Context context) {
return new PhoneWindow(context);
}
Window:是一个抽象类,提供绘制窗口的通用API。
PhoneWindow :是Window的唯一的实现类,每个Activity都会有一个PhoneWindow,它是Activity和整个View交互的接口。该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。
DectorView:是PhoneWindow的内部类,继承自FrameLayout。
1.2 调用PhoneWindow对象的setContentView方法
一层层下来,发现Activity的setContentView()
实际上是执行的是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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
以上代码分析:
1 . 判断mContentParent
是否为空。从名字可以看出,这是父容器,它是一个ViewGroup类型的对象,是真正的content的Parent,如果是第一次调用,会调用installDecor()
,在这个方法中会先判断DectorView是否为空,为空就先创建DectorView对象mDecor,然后调用generateLayout(mDecor)
得到mContentParent
;如果不是第一次调用,会先清除mContentParent
中的子view。
2 . mLayoutInflater.inflate(layoutResID, mContentParent);
将传入的资源文件转换成View树,再添加到mContentParent中(mLayoutInflater 在PhoneWindow的构造函数中通过mLayoutInflater = LayoutInflater.from(context)
得到)。
再来看一下其他的两个setContentView()
:
@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();
}
}
可以看到,其实做的事情都差不多,只不过多设置了LayoutParams
,因为传入的View,就不需要像第一个那样还从xml文件中解析出来,可以直接将view添加到mContentParent
中。
小结:比较3个setContentView
从上面的代码中,我们可以看到,第一个setContentView
是通过反射解析传入的布局文件,然后添加到mContentParent
,而后两个setContentView
是直接将传入的View添加到mContentParent
,需要注意的是,每次反射拿到的View都是重新创建的,就算两次setContentView加载的是同一个布局文件,控件的实例也是不一样的,如传入的是View/ViewGroup就能保证传入的是同一组控件。
1.3 installDecor()实例化DectorView对象
现在来看一下刚刚提到的installDecor()
,初始化mDecor ,创建mContentParent
,根据窗口的风格修饰,选择对应的修饰布局文件,这里内容太多,省略了。
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) {
//根据窗口的风格修饰,选择对应的修饰布局文件
mContentParent = generateLayout(mDecor);
......
}
}
1.4 generateLayout()创建mContentParent
接下来看generateLayout()
创建mContentParent 的过程。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//根据当前的主题设置窗口属性
......
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
//根据当前的窗口属性选择相对应的布局
WindowManager.LayoutParams params = getAttributes();
......
//将相应的布局文件转成view添加到窗口视图对象decor中
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");
}
......
return contentParent;
}
这段代码所做的事情:
- 根据当前的主题设置窗口属性;
- 根据当前的窗口属性选择相对应的布局;
- 将相应的布局文件转成view添加到根视图对象decor/mDecor中
View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
,需要注意的是,这里的layoutResource并不是我们传入的资源文件,而是系统定义的。 - 从根布局中找到id为
ID_Android_CONTENT
的ViewGroup赋值给contentParent
,也就是上文的mContentParent
。
总结:Activity,PhoneWindow,DectorView,mContentParent之间的关系
通过上面的分析,我们来看一下彼此之间的关系,有助于理解。
DecorView继承于FrameLayout,然后它有一个子view即LinearLayout,方向为竖直方向,其内有两个FrameLayout,上面的FrameLayout即为TitleBar之类的,下面的FrameLayout即为我们的ContentView,所谓的setContentView就是往这个FrameLayout里面添加我们的布局View的!
2.PhoneWindow的setContentView最后的回调
上面分析了加载视图到父容器mContentParent
中,现在我们看一下setContentView()
中的最后一步。
@Override
public void setContentView(int layoutResID) {
......
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
至此,已经分析完了将传入的布局文件添加到View的整个过程。接下来可以看到,它先创建了一个CallBack回调接口,在加载完以上的View到根布局之后,就会调用这个回调接口,顺便说一下,cb.onContentChanged()
方法在Activity中是一个空方法,我们可以在自定义的Activity中覆写这个方法。
现在看getCallback()
是由Window提供的,PhoneWindow并没有实现,继续往下看,发现Window中有一个public void setCallback(Callback callback)
方法,接收到外部传入的callback,赋值给内部的mCallback 。那么这个外部方法在哪里调用呢?这个就要说一下Activity的启动了。
3.Activity的启动
在Activity加载时会先创建一个activity实例,然后调用activity的attach方法完成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, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachActivity(this, mContainer, null);
//1.创建窗口对象,是一个PhoneWindow实例
mWindow = PolicyManager.makeNewWindow(this);
//2.设置回调
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
......
mToken = token;
//3.将创建的WindowManager注入窗口对象以便管理窗口的视图对象
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
//4. 获取窗口管理器
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}
在这个方法中,完成了上文提到的Window对象的创建。
为Window对象设置回调,也就是上面
setContentView()
中最后获取到cb。这里设置的回调就是Activity自己,再看Activity,它实现了Window.Callback, KeyEvent.Callback
两个回调接口。其中KeyEvent.Callback
接口中声明了处理手势事件的方法(onKeyDown按下,onKeyUp抬起,onKeyLongPress长按...),而Window.Callback
声明了一些事件分发的函数,关于View的事件分发,可以看这个 View的事件分发机制 ,从这里可以看出activity本身不具备处理用户的事件的能力。为mWindow设置
WindowManager
,WindowManager
主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等.获取Window的WindowManager的实现类WindowManagerImpl保存在activity的mWindowManager中。
3.1 关于mWindow.setWindowManager()方法
从setWindowManager方法中可以发现,这里返回的是WindowManagerImpl
对象,WindowManager
是一个接口,而WindowManagerImpl
是它的实现类,这里调用createLocalWindowManager()
。
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
WindowManagerImpl中的createLocalWindowManager()
方法
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
3.2 将生成的窗口视图对象是添加到手机屏幕
我们知道,Activity的视图是在Activity的生命周期onResume()
方法执行之后才会显示的,这是因为在ActivityThread的handleResumeActivity
方法中调用了Activity的onResume()
方法,关于Activity的启动这一部分内容我还没有看过,“老罗的Android之旅”中有这一部分内容的详细分析,有兴趣的可以看一下。在这个函数中,会调用activity的makeVisible方法经WindowManagerImpl将DecorView展示出来。这里的getWindowManager()
就是之前设置的WindowManager。最后就Activity就可以请求WindowManagerService将视图绘制到屏幕上了。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
总结
总结一下整个View的加载过程:
首先在Activity启动时,在
attach
方法中先创建Activity的窗口对象,它是PhoneWindow
类型,每个Activity都有一个窗口对象,然后为这个窗口设置各种事件的回调,还要注册其对应的窗口管理器,用来管理窗口的一些状态,属性,view的更新等。当调用Activity的
onCreat
方法时,会调用设置布局文件,Activity的setContentView
其实调用的是Activity的窗口对象PhoneWindow的setContentView,PhoneWindow有一个内部类DectorView(FrameLayout的子类)
,它是整个窗口下的根View,内部包含两个FrameLayout,一个根据主题样式来进行TitleBar之类设置,一个就是用来装我们传入的布局文件中的view,这个就是mContetntParent
。第一次调用PhoneWindow
的setcontentView
方法会先创建DectorView
,进行一些初始化的设置,然后解析系统的资源文件到DectorView
中,接着会找到id为ID_Android_CONTENT
的FrameLayout
,将其赋值给mContetntParent
,用来放我们传入的资源文件解析出来的view.这些初始化的设置完成之后,就是处理我们调用
setContentView
时传入的布局文件了,如果传入的资源文件id,会调用反射机制解析xml文件,再把解析出来的各个view加到mContetntParent
,如果传入的是View
,那么直接加到mContetntParent
就可以,最后就是系统在调用onResume
之后,经之前设置的WindowManager
将整个DecorView
展示出来。