理解Activity的窗口层级在日常开发中也有着很重要的作用,举个例子我们日常开发中为了用户体验都会在耗时操作时显示一个loading动画,最容易想到的办法就是给页面套一层FrameLayout,但是大部分情况下我们都需要给页面添加loading,一个个页面添加重复的工作量太多,有没有简单的办法呢?
当我们了解了窗口的布局层级后就会发现系统原本就会给我们的布局外面额外添加一个id为android.R.id.content的FrameLayout,那么我们就可以直接通过这个id获取到这个FrameLayout,将loading添加进去,我们在开发时又都会有一个BaseActivity来做一些统一的处理,这样我们即节省了重复的工作,随时可替换loading动画,又减少了布局层级。再比如还可以给页面添加蒙版等等同样的原理。
先上一张笔者根据源码画的Activity的窗口层级示意图
Activity的window创建是在Activity启动过程中完成的,Activity的启动过程这里不再赘述,详情见Activity启动流程图。
在ActivityThread类的performLaunchActivity中调用Activity的attach()方法创建Window的唯一实现类PhoneWindow
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, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this);//这里创建Window
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
......
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());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}
我们知道Activity创建时会回调onCreate方法我们会在此方中调用setContentView()加载Activity布局,其内部实际上是调用了PhoneWindow的setContentView()方法,代码如下
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
我们进入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();//初始化mDecor和mContentParent
} 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();
}
}
既然setContentView()是将我们写的xml布局给加载进来,那我们就找一下是哪里吧这个layoutID给inflate到内存中的,跟进一下这个参数,果然第17行 mLayoutInflater.inflate(layoutResID, mContentParent)找到了我们熟悉的代码。所以mContentParent就是我们加载布局的父布局!
假设把我们手写的Activity布局视图叫myView,现在按布局从外向内层级排序就是mContentParent--->myView
mContentParent又是什么呢,它是什么时候被添加到Window上的呢? 进入setContentView()我们就可以看到,方法内先对mContentParent进行判空操作,所以它的初始化一定在installDecor中,代码如下:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();//初始化mDecor
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);//获取mContentParent
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
mDecorContentParent.setWindowCallback(getCallback());
if (mDecorContentParent.getTitle() == null) {
mDecorContentParent.setWindowTitle(mTitle);
}
final int localFeatures = getLocalFeatures();
for (int i = 0; i < FEATURE_MAX; i++) {
if ((localFeatures & (1 << i)) != 0) {
mDecorContentParent.initFeature(i);
}
}
mDecorContentParent.setUiOptions(mUiOptions);
if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
(mIconRes != 0 && !mDecorContentParent.hasIcon())) {
mDecorContentParent.setIcon(mIconRes);
} else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
mIconRes == 0 && !mDecorContentParent.hasIcon()) {
mDecorContentParent.setIcon(
getContext().getPackageManager().getDefaultActivityIcon());
mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
}
if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
(mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
mDecorContentParent.setLogo(mLogoRes);
}
// 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) && !mIsStartingWindow) {
invalidatePanelMenu(FEATURE_ACTION_BAR);
}
} else {
mTitleView = (TextView)findViewById(R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(
R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
}
}
if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
......
}
}
在第10行代码我们可以看到通过generateLayout(mDecor)获取到了一个mContentParent对象,至于参数mDecor暂且不管,我们先看generateLayout(mDecor)方法内部是怎么实现的:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
......
// 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) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));//添加到mContentRoot到mDecor
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);//获取mContentParent
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
......
return contentParent;
}
通过其返回值contentParent搜索此方法,发现第71行contentParent是通过Window的findViewById得到的,进入到方法内:
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
它又是通过调用mDecor的findViewById()方法得到的。mDecor又是什么呢?前面似乎见过这个变量,我们在跟进generateLayout(mDecor)时曾暂时忽略了mDecor这个参数。回到installDecor()方法,方法内首先又是mDecor的判空操作,进入generateDecor()方法内看到它直接new了一个DecorView对象。
DecorView类直接继承于FrameLayout,初始内部并没有添加子View,那么它findViewById是从哪里找到mContentParent视图的呢?
我们从generateLayout(DecorView decor)方法中findViewById()向前追溯2行即发现有这么一行代码
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
mDecor中添加了一个名为in的布局,再向前追溯mDecor中并没有再添加其他的视图。这说明mContentParent一定是从in视图中找到的,向上追溯代码in视图则是Window根据不同的features主题特征加载进来的布局视图mContentRoot。
setContentView从创建顶级视图DecorView开始到将我们的布局添加到视图树结束,整个流程到这里就分析完毕了,按照布局层级顺序为mDecor (FramLayout) ---> mContentRoot (ViewGroup)---> mContentParent (ViewGroup)--->myView (setContent添加布局),如文章开始图片所展示。