背景 为什么要写这篇呢,其实想写一篇比较深入的文章很久了,只是一直比较懒和各种接口没去花精力去实施,正好有个元旦假期,花了点时间看了一些博客再加上自己的分析,然后想记录下来,不敢说多么的精彩和深入,就当个笔记。也算是frameWork
层的初步探索吧。开始吧。
setContentView 在Android里面,去设置布局最常见的就是在onCreate
方法里面使用setContentView(int layoutResID)
这个函数把定义的XML文件设置进去,跟进去看看,发现其实有三个重载的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
但是最终都是调用了getWindow().setContentView
,这个getWindow()
又是啥?
1
2
3
public Window getWindow () {
return mWindow;
}
返回的是一个Window
对象,这个Window
是个抽象类(不那么啰嗦了),真正作用的是唯一的一个实现类PhoneWindow
,在attach
里面可以看到mWindow = new PhoneWindow(this);
再回到前面还有个方法initWindowDecorActionBar();
看名字是初始化Window装饰ActionBar,进去看看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
* Creates a new ActionBar, locates the inflated ActionBarView,
* initializes the ActionBar with the view, and sets mActionBar.
*/
private void initWindowDecorActionBar () {
Window window = getWindow();
window.getDecorView();
if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null ) {
return ;
}
mActionBar = new WindowDecorActionBar(this );
mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
mWindow.setDefaultIcon(mActivityInfo.getIconResource());
mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
}
这里看到又调用了getWindow
而且下面还有个window.getDecorView();
上面的注释看到,这个函数执行之后就初始化了window
的各种feature flags
所以这就是为什么我们常见的设置feature
一定要在setContentView
之前了,因为在执行setContentView
之后,就把Window的相关特征标志给初始化了,再去设置也没什么卵用。这个window.getDecorView()
在PhoneWindow
是这个样子的:1
2
3
4
5
6
7
@Override
public final View getDecorView () {
if (mDecor == null ) {
installDecor();
}
return mDecor;
}
这个installDecor
在setContentView
里面也有使用,还是回去吧,看看setContentView
里面是什么样子的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void setContentView (int layoutResID) {
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();
}
}
这里有两个疑问,mContentParent
是啥?installDecor();
里面到底干了啥?下面再细说一下。
installDecor() 直接进入这个方法看看:
1
2
3
4
5
6
7
8
9
10
private void installDecor () {
if (mDecor == null ) {
mDecor = generateDecor();
...
}
if (mContentParent == null ) {
mContentParent = generateLayout(mDecor);
}
}
这里主要关注generateDecor
和generateLayout(mDecor)
,这里顺便把上面的mContentParent
的问题提到了一点,mContentParent
就是在这里初始化的,先看上面的mDecor = generateDecor();
进去看到这个:
1
2
3
protected DecorView generateDecor () {
return new DecorView(getContext(), -1 );
}
这个DecorView
是啥?他是PhoneWindow
的一个静态内部类:
1
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
看到吧,其实就是个继承FrameLayout
的ViewGroup
,看一张图,转自工匠若水
先说个结论:mContentParent
就是DecorView
里面的那个content
,我们平时写的布局都是扔到了这个里面,所以叫做setContentView()
,而DecorView
就是包裹在外面的一层更根的布局。。不信可以进去generateLayout(mDecor)
方法里面看看,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected ViewGroup generateLayout (DecorView decor) {
TypedArray a = getWindowStyle();
int layoutResource;
int features = getLocalFeatures();
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;
}
总结一下setContentView
的过程:
创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
调用generateLayout(DecorView decor)
依据Feature等style theme创建不同的窗口修饰布局文件,然后
1
2
3
4
5
View in = mLayoutInflater.inflate(layoutResource, null );
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
到这,setContentView
就差不多走完了,这个工作我们一般是在onCreate()
里面进行,但是还远没有到显示的地方,下面来简单说一下显示的过程,一个Activity的开始实际是ActivityThread
的main方法(这个还没分析过Activity的启动过程,先就这么认为吧),当启动Activity调运完ActivityThread
的main方法之后,接着调用ActivityThread
类performLaunchActivity
来创建要启动的Activity组件,在创建Activity组件的过程中,还会为该Activity组件创建窗口对象和视图对象;接着Activity组件创建完成之后,通过调用ActivityThread类的handleResumeActivity
将它激活。看一下这个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
final void handleResumeActivity (IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null ) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
if (a.mVisibleFromClient) {
a.mWindowAdded = true ;
wm.addView(decor, l);
}
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
}
其中的makeVisible
方法:
1
2
3
4
5
6
7
8
void makeVisible () {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true ;
}
mDecor.setVisibility(View.VISIBLE);
}
到现在,整个界面才真正的显示出来,所以当调用了onResume
方法,界面也不一定是显示的。这里还有几个问题,mLayoutInflater.inflate(layoutResID, mContentParent)
这个到底是怎么把XML解析成view的,findViewById
是怎么找view的,还有就是WindowManager
是啥?他和前面提到的Window
,PhoneWindow
,DecorView
啥关系?下面会再来扯一扯。
findViewById 我们平时在Activity
里面去使用,跟进去看看
1
2
3
public View findViewById (@IdRes int id) {
return getWindow().findViewById(id);
}
好吧,熟悉吧,再进去看看
1
2
3
4
@Nullable
public View findViewById (@IdRes int id) {
return getDecorView().findViewById(id);
}
看,到了view的地方,再进去
1
2
3
4
5
6
public final View findViewById (@IdRes int id) {
if (id < 0 ) {
return null ;
}
return findViewTraversal(id);
}
再进去看看这个递归函数是怎么回事:
1
2
3
4
5
6
protected View findViewTraversal (@IdRes int id) {
if (id == mID) {
return this ;
}
return null ;
}
就是返回自己啊,但是仔细想想,还是得去ViewGroup
去看,具体原因就不扯了,这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected View findViewTraversal (@IdRes int id) {
if (id == mID) {
return this ;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0 ; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0 ) {
v = v.findViewById(id);
if (v != null ) {
return v;
}
}
}
return null ;
}
就是去遍历ViewGroup
里面的View
,然后去调用View
的findViewById
,所以我们的findViewById
都是在DecorView
这个ViewGroup
里面去查找的。
mLayoutInflater.inflate 当需要加载个XML布局文件的时候,一般这样使用LayoutInflater.from(this).inflate()
,里面有一些函数的重载,但是最终都是走到了这里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public View inflate (@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")" );
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
使用XML解析器去解析XML文件,关键是第10行,进去看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public View inflate (XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
View result = root;
...
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true" );
}
rInflate(parser, root, inflaterContext, attrs, false );
} else {
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null ;
。。。
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true );
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
。。。
return result;
}
}
所以从上面的代码可以总结一下:
inflate(xmlId, null); 只创建temp的View,然后直接返回temp。
inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。
inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。
inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。
这里也引出了一个问题,就是有时候我们通过View的layout_width
和layout_height
来设置view的大小,然后通过inflate
解析之后发现并不管用,这里的代码就可以说明问题,其实这两个属性并不是用来设置view的大小的,而是用来设置view在ViewGroup中的大小,(有点晕?后面会有专门的一篇来说这个),所以叫做layout_width
而不是直接叫做width
。简单举个例子说一下:
mInflater.inflate(R.layout.textview_layout, null)
不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
mInflater.inflate(R.layout.textview_layout, parent)
能正确显示我们设置的宽高是因为我们的View在设置setLayoutParams时params = root.generateLayoutParams(attrs)
不为空。
其他就不多说了,也就是说只有这是了父布局才能正确的显示宽和高,同时可以注意到,在Activity
中指定布局宽高的时候是可以正确显示的,这不正好说明了还存在更底层的一层id为content的FrameLayout吗。
参考:
【凯子哥带你学Framework】Activity界面显示全解析
Android应用setContentView与LayoutInflater加载解析机制源码分析
PhoneWindow源码