【本文出自大圣代的技术专栏 http://blog.csdn.net/qq_23191031】
【转载烦请注明出处,尊重他人劳动成果就是对您自己的尊重】
不喜欢看文字的可以直接到文字尾,看图说话。
1, 前言
- 在前面《【Android 控件架构】详解Android控件架构与常用坐标系》的文章中我们提到了
setContentView()
方法,当时只是匆匆带过,并没有阐明具体流程。而这篇文章就是从Activity中的setContentView()
方法出发结合上篇的视图框架,详细分析setContentView()
的工作原理。还是贴一张图复习一下吧。
- 从上面的文章中我们知道
setContentView()
方法是用来设置ContentView布局地,当系统调用了setContentView()
方法所有的控件就得到了显示,但是你有想过Android系统是如何让xml文件加载到界面并显示出来的呢?setContentView()
中具体是如何实现的呢?就让我们在这些疑问来进入下面的探讨吧。
2 从setContentView说起(基于Api 25 Android 7.1.1)
本来是想基于Api 26来看的,可是后来才想起来 Android 8.0的源码还没发布。。。
2-1 Activity源码中的setContentView
经过阅读Android的源码发现,系统为我们提供了三个setContentView()
的重载方法,他们都调用了getWindow()
中的setContentView()
方法。
public void setContentView(@LayoutRes 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()
方法有事做什么的呢,咱们继续往下看。
2-2 关于窗口Window类的一些关系
getWindow()
的作用
/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
// 如果返回为null表示,则表示当前Activity不在窗口上
public Window getWindow() {
return mWindow;
}
...
mWindow = new PhoneWindow(this, window);
通过源码我们可以看到getWindow()
方法返回的就是PhoneWindow的实例对象(PhoneWindow是抽象类Window的唯一实现类 PhoneWindow在线源码地址)
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private final static String TAG = "PhoneWindow";
...
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// 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;
private ViewGroup mContentRoot;
...
}
而在PhoneWindow
中我们看到了作为成员变量的 mDecor
,(在Android 7.1.1中DecorView已经不再是PhoneWindow的内部类了,而且包都换了,有图有真相)。
查看DecorView
之后发现public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
,看见没有,DecorView
才是Activity
的根布局(root view),他继承了 FrameLayout
负责Activity
视图的加载,而DecorView
本身则是由PhoneWindow
加载的。PhoneWindow
是如何加载DecorView
的呢,咱们带着问题继续往下看
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
private static final String TAG = "DecorView";
private static final boolean DEBUG_MEASURE = false;
private static final boolean SWEEP_OPEN_MENU = false;
// The height of a window which has focus in DIP.
private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
// The height of a window which has not in DIP.
private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
....
}
一言不可就上图:
2-3 PhoneWindow中的setContentView方法
在Window
类中setContentView
方法是抽象的,所以我们直接去看PhonWindow
类中关于 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) {
//创建DecorView,并添加到mContentParent上
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()) {
//回调通知表示完成界面加载
cb.onContentChanged();
}
}
源码中的第一步就是验证mContentParent
是否为 null
,如果为null则表示程序是第一次运行,执行installDecor
。如果不为null则会判断当前是否设置了FEATURE_CONTENT_TRANSITIONS(这个属性表示内容加载时需不需要过场动画,默认为false)。如果没有使用过场动画则移除mContentParent
中的所有view(所以说 setContentView
方法可以多次调用,因为他会移除掉所有的控件);
如果在初始化mContentParent
之后,用户设置了启用转场动画则使用Scene
开启过度,否则mLayoutInflater.inflate(layoutResID, mContentParent);
将我们的资源文件通过LayoutInflater
对象转化为控件树添加到mContentParent
中。
再来看下PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,如下:
422 @Override
423 public void setContentView(View view) {
424 setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
425 }
@Override
428 public void setContentView(View view, ViewGroup.LayoutParams params) {
429 // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
430 // decor, when theme attributes and the like are crystalized. Do not check the feature
431 // before this happens.
432 if (mContentParent == null) {
433 installDecor();
434 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
435 mContentParent.removeAllViews();
436 }
437
438 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
439 view.setLayoutParams(params);
440 final Scene newScene = new Scene(mContentParent, view);
441 transitionTo(newScene);
442 } else {
443 mContentParent.addView(view, params);
444 }
445 mContentParent.requestApplyInsets();
446 final Callback cb = getCallback();
447 if (cb != null && !isDestroyed()) {
448 cb.onContentChanged();
449 }
450 mContentParentExplicitlySet = true;
451 }
看见没有,我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。
所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。
2-4 installDecor()方法 源码分析
2614 private void installDecor() {
2615 mForceDecorInstall = false;
2616 if (mDecor == null) {
2617 mDecor = generateDecor(-1);
2618 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
2619 mDecor.setIsRootNamespace(true);
2620 if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
2621 mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
2622 }
2623 } else {
2624 mDecor.setWindow(this);
2625 }
2626 if (mContentParent == null) {
//根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
2627 mContentParent = generateLayout(mDecor);
//......
2674 } else {
2675 mTitleView = (TextView) findViewById(R.id.title);
2676 if (mTitleView != null) {
//根据FEATURE_NO_TITLE隐藏,或者设置mTitleView的值
2677 if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
2678 final View titleContainer = findViewById(R.id.title_container);
2679 if (titleContainer != null) {
2680 titleContainer.setVisibility(View.GONE);
2681 } else {
2682 mTitleView.setVisibility(View.GONE);
2683 }
2684 mContentParent.setForeground(null);
2685 } else {
2686 mTitleView.setText(mTitle);
2687 }
2688 }
2689 }
我在源码中发现了一个很重要的东西,请看第2677行!!!,这就在最根本上解释了:为什么要在setContentView()
方法之前设置requestWindowFeature(Window.FEATURE_NO_TITLE)
才能不显示TitleActionBar部分,达到全屏的效果。
言归正传,installDecor()
方法一进来就判断mDcor
是否为空,为空怎么办创建一个喽,咦generateDecor(-1)
传一个 -1 是什么鬼???代码规范呢!Google也可以这么写代码么??......咳咳。
2263 protected DecorView generateDecor(int featureId) {
//......
2281 return new DecorView(context, featureId, this, getAttributes());
2282 }
ps:怎么又一大堆,看来7.1.1的源码和5.1.1的差异真是不小啊。啥,Androdi5.1.1里面的长啥样?
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
不看不知道,一看吓一跳。看见没有,一共两行。这里就不展开讨论了.....
2-5 generateLayout()方法 源码分析
在源码 2626行,我们看到当 mContentParent == null
的时候使用generateLayout(mDecor)
方法创建一个mContentParent
出来。generateLayout(mDecor)
看名字好像倒是像用来设置layout的。
2284 protected ViewGroup generateLayout(DecorView decor) {
2285 // Apply data from current theme.
//首先通过WindowStyle中设置的各种属性,对Window进行requestFeature或者setFlags
2287 TypedArray a = getWindowStyle();
2288
//...
2299 mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
2300 int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
2301 & (~getForcedWindowFlags());
2302 if (mIsFloating) {
2303 setLayout(WRAP_CONTENT, WRAP_CONTENT);
2304 setFlags(0, flagsToUpdate);
2305 } else {
2306 setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
2307 }
2309 if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
2310 requestFeature(FEATURE_NO_TITLE);
2311 } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
2312 // Don't allow an action bar if there is no title.
2313 requestFeature(FEATURE_ACTION_BAR);
2314 }
//....
//...根据当前sdk的版本确定是否需要menukey
2413 WindowManager.LayoutParams params = getAttributes();
2491 // Inflate the window decor.
2492
2493 int layoutResource;
2494 int features = getLocalFeatures();
//......
//根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值
//把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值
2495 // System.out.println("Features: 0x" + Integer.toHexString(features));
2496 if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
2497 layoutResource = R.layout.screen_swipe_dismiss;
2498 } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
2499 if (mIsFloating) {
2500 TypedValue res = new TypedValue();
2501 getContext().getTheme().resolveAttribute(
2502 R.attr.dialogTitleIconsDecorLayout, res, true);
2503 layoutResource = res.resourceId;
2504 } else {
2505 layoutResource = R.layout.screen_title_icons;
2506 }
2507 // XXX Remove this once action bar supports these features.
2508 removeFeature(FEATURE_ACTION_BAR);
2509 // System.out.println("Title Icons!");
2510 } else if {
//......
2552
2553 mDecor.startChanging(); //通知 开始改变
2554 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
2555
2556 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//......
2604 mDecor.finishChanging();//通知 改变完成
2605
2606 return contentParent;
2607 }
}
从整体角度来讲这个方法就是根据用户设置的风格、标签为窗口选择不同的主布局文件,DecorView做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。 哎!我怎么没看见DecorView添加布局的代码呢?别急下边就告诉你怎么回事。
在进入这个方法时,系统就会调用getWindowStyle()
在当前的Window的theme中获取我们的Window属性,对我们的Window设置各种requestFeature,setFlags等等。
getWindowStyle()
为抽象类Window
提供的方法,具体源码如下:
665 public final TypedArray getWindowStyle() {
666 synchronized (this) {
667 if (mWindowStyle == null) {
668 mWindowStyle = mContext.obtainStyledAttributes(
669 com.android.internal.R.styleable.Window);
670 }
671 return mWindowStyle;
672 }
673 }
我们顺藤摸瓜找到属性位置 源码地址
所以这里就是解析我们为Activit设置theme的地方,至于theme一般可以在AndroidManifest.xml文件中设置。
接下来就到关键的部分了,2494-2510行:通过对features和mIsFloating的判断,获取不同的主布局文件为layoutResource进行赋值,值可以为R.layout.screen_custom_title;R.layout.screen_action_bar;等等。
经过上面的源码我们可以看到设置features,除了theme中设置的,我们还可以在代码中进行:
//通过java文件设置:
requestWindowFeature(Window.FEATURE_NO_TITLE);
//通过xml文件设置:
android:theme="@android:style/Theme.NoTitleBar"
其实我们平时requestWindowFeature()设置的features值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。两方式具体流程不同,但是效果是一样的。
所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。
我靠,我还是没看见DecorView添加布局的代码啊 ,这就来:
源码 2554行,进行了如下操作:
2554 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
看名字是在进行资源文件的加载,具体是怎么操作的呢:
1801 void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
//......
1813 final View root = inflater.inflate(layoutResource, null);
//......
1824 addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
//......
1826 mContentRoot = (ViewGroup) root;
//......
1828 }
在源码1824行,系统将 layoutResource 所代表的主布局文件。添加到 DecorView 中,而在源码中第 2556行我们可以看到,系统又在DecorView中需找一个ID_ANDROID_CONTENT
布局赋值给contentParent
。
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
而 ID_ANDROID_CONTENT
又是个什么东西呢?我在Windows
抽象类中找到了它的源码。注释说的很明确,每一个主布局都拥有id为content
的控件。通过mContentRoot = (ViewGroup) root;
我们可以清楚的知道,layoutResource
既为整个窗口的根布局。
/**
* The ID that the main layout in the XML layout file should have.
*/
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
随手贴几个布局文件加以证明:
R.layout.screen_simple:
R.layout.screen_simple_overlay_action_mode
同样在Windows
抽象类中找到了findViewByID
方法的源码,findViewByID
的作用就是将在DecoreView
中需找 id
为content
的FragmentLayout
赋值给 contentParent
1252 /**
1253 * Finds a view that was identified by the id attribute from the XML that
1254 * was processed in {@link android.app.Activity#onCreate}. This will
1255 * implicitly call {@link #getDecorView} for you, with all of the
1256 * associated side-effects.
1257 *
1258 * @return The view if found or null otherwise.
1259 */
1260 @Nullable
1261 public View findViewById(@IdRes int id) {
1262 return getDecorView().findViewById(id);
1263 }
最后generateLayout()
的最后系统还会调用Callback
接口的成员函数onContentChanged
来通知对应的Activity组件视图内容发生了变化。至此Android setContentView()
方法分析完成。
3,总结
图片被缩小了不清楚,不要紧。请右键 - 在新标签中打开图片。
由此就组成了我们在《【Android 控件架构】详解Android控件架构与常用坐标系》一篇中提到的视图框架(图中contentView就是源码中的contentParent)
4,参考:
如果说我比别人看得更远些,那是因为我站在了巨人的肩上
- 在线源码地址
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android 源码解析 之 setContentView
- Android UI 窗口体系 —— 源码阅读