作为应用层开发的同学,对Android UI的显示绘制和事件响应应该不陌生,本文将在Android 13
源码的基础上,来介绍setContentView的流程逻辑
前言
下面是我们业务开发同学经常接触到的模板代码,在Activity的onCreate
方法中调用setContentView
将XML编写好的activity_main.xml布局绑定到界面上
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
之所以说是模板代码,是因为大家都这么写,那可以不这么写吗?结论肯定是可以的
比如我们完全可以先不设置ContentView,等待业务网络请求回来后,再setContentView显示也是可以的,说到这其实还可以引申出布局优化,页面启动优化,如我们大型App的首页,任务繁重,UI的渲染肯定是极其耗时的,这个时候就可以先让setContentView显示一个较为简单的Layout,待各个业务初始化成功后,再一步一步显示出真实的UI出来,这也就是所谓的懒加载,是一个对用户友好,对我们KPI也友好的方式,不过这个太依赖编程者的功力和对业务的熟练程度。
到这里,应用层开发基本上就结束了,剩下要做的就是findViewById
,和业务处理数据回显
的操作。那当我们调用这个api背后到底发生了什么呢?
搜索Android源码,发现有两处地方调用了setContentView
AppCompatActivity
继承自Activity,重写了setContentView
,两者本质上都会调用到window的setContentView,区别在于AppCompatActivity将我们创建的布局进行了再一层的包装处理,下面我们分开探索其中的逻辑
Activity setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
这里通过getWindow获取到了一个Window对象,官方对Window的解释是:
顶层窗口和行为的抽象,当前接口的实例应用作添加到窗口管理器的顶级视图,它提供了标准的 UI 策略,例如背景、标题区域、默认键处理等。这个抽象类的唯一现有实现是 android.view.PhoneWindow
由此可知最终会调用PhoneWindow的setContentView方法,这里还有一个initWindowDecorActionBar的逻辑,主要是用来处理一些关于ActionBar的逻辑
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//当ContentParent为null时,去初始化DecorView
} 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();
}
mContentParentExplicitlySet = true;
}
这里先判断了mContentParent是否为null(后面我都会以ContentParent命名来介绍),是就调用installDecor去重新创建,否则就将我们传入的layoutResID对应的布局填入 mContentParent中。
这里还判断了hasFeature()
来检测标志位,主要是作用于Activity的过渡动画,我们可以在Activity中通过requestWindowFeature等方式设置,如Activiy A跳转到Activity B,我们就可以getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
来开启动画
回到mContentParent
那这个mContentParent是什么呢?又是怎么创建的?我们接着往下看installDecor()
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
上面可以总结成2步:
第一步首先创建DecorView,第二步创建ContentParent,并将DecorView传入,最终ContentParent会被add 到DecorView中; 我们还是来看下DecorView的创建,发现其实很简单,就是直接new了一个
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}
然后我们再看看看DecorView的定义
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
}
发现DecorView其实就是一个FrameLayout,并且创建的时候将PhoneWindow传入了,由此就将DecorView和Window进行了联系,如根据Window的一些属性去设置View等
接下来我们再看ContentParent的创建
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource;
int features = getLocalFeatures();
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
layoutResource = R.layout.screen_title_icons;
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
...
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
...
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
...
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}
这里会根据我们设置features的不同,加载不同的xml布局文件,如我们在Activity中通过requestWindowFeature等方式设置,就可改变getLocalFeatures
获取的值,接着调用了onResourcesLoaded()
,将上面加载的xml文件add到DecorView中
如果我们不对Features进行设置,则会走默认的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>
这里就有我们很熟悉的一个逻辑,即@android:id/content
,开发中我们可能会经常看到findViewById(android.R.id.content)这样的代码,这里找到的就是ContentParent中的FrameLayout
然后我们再来看看onResourcesLoaded
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
这里最终会根据mDecorCaptionView的创建结果,去决定是将上一步传入的xml文件id填充到哪,mDecorCaptionView的创建依赖于之前的fetures,如浮窗、dialog等,如果不是这些类型,则直接将我们的布局add到DecorView中
至此,经过了DecorView的创建,和ContentParent的创建,我们自己书写的布局就被添加到顶层View中了
此时我们已经得到了完整的DecorView,其中就包含了我们的布局,到这我们只是走完了创建这个步骤,那这个顶层View是如何绑定到Window中的呢,这得回归到ActivityThread中的handleResumeActivity方法
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
a.getActivityToken());
}
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;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
...
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
...
}
}
...
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
if (r.activity.mVisibleFromClient) {
//使View可见
r.activity.makeVisible();
}
}
}
这里会先判断window的属性,如之前是否add过,是否有经历onStart方法,是否已经被Finished,最终会调用ViewManager.addView
,将DecorView填充到Window中,这里的ViewManager实例就是Activity中的mWindowManager
对象,实现类是WindowManagerImpl
,然后最终会调用到这里的addView方法
这里有一个逻辑是在调用WindowManagerImpl.addView
之前,先设置View不可见,添加过后,再设置为可见
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyTokens(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
WindowManagerImpl
中的addView最终会调用到WindowManagerGlobal
中的addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
} else {
root = new ViewRootImpl(view.getContext(), display,
windowlessSession);
}
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
上面根据条件最终创建了ViewRootImpl,然后将DecorView设置进去,
ViewRootImpl是管理DecorView和WMS的桥梁,用来收发一些WMS的通知,每次addView()添加窗口时,都会创建一个新的ViewRootImpl
;
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
requestLayout();
try {
...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
mTempControls);
} catch (RemoteException e) {
。。。
}
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
。。。
}
}
可以看到,这里通过和WMS远程调用,mWindowSession.addToDisplayAsUser最终会调用到WindowManagerService的addWindow
才最终将PhoneWindow显示在屏幕上,同时可以取得返回值,后面根据这些返回值,去抛出了一系列异常,相信其中的一些Exception我们并不陌生,如Unable to add window -- token
这里值得注意的是在调用WMS方法之前,调用了requestLayout
,由此展开了View的measure、layout、draw三件套方法流程,完成了视图最终在屏幕上的显示
到这里,我们明白了setContentView后,我们的布局是怎么加载到DecorView中,同时DecorView又是怎么和Window进行绑定,然后Window又是何时以怎样的方式将DecorView呈现给用户的。
AppcompatActivity setContentView
接下来我们再回到前面说的AppcompatActivity的setContentView()
,看源码得知AppcompatActivity将操作给了代理类AppCompatDelegateImpl
,我们找到AppCompatDelegateImpl的setContentView方法
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
与之前Activity不同的是,这里通过findViewById的方式直接找到了ContentParent,并且将当前我们自己写的布局add
进去了.知道了之前的流程,我们知道会先创建DecorView,接着创建ContentParent,那这里是怎么创建的呢,我们接着看ensureSubDecor()
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
mSubDecorInstalled = true;
}
}
然后看createSubDecor()
private ViewGroup createSubDecor() {
...
mWindow.getDecorView();
ViewGroup subDecor = null;
if (!mWindowNoTitle) {
if (mIsFloating) {
。。。
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
} else {
。。。
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
mWindow.setContentView(subDecor);
}
可以发现,这里默认调了1次mWindow.getDecorView()
我们来看看getDecorView的实现
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
看到这里就明白了,getDecorView也是会间接的去调用了installDecor,去完成DecorView的创建和ContentParent的创建工作,那我们AppcompatActivity相比于Activity有其它不同吗?答案是有的,继续从createSubDecor
往下看,发现会根据window的一些属性,例如是否悬浮弹窗、是否有标题主题等等,去加载不同的xml布局得到subDecorView,到这里其实从变量命名也可以看出,subDecor最终也会是DecorView的一个子View,假设我们设置的属性命中了abc_screen_toolbar.xml,布局如下
<androidx.appcompat.widget.ActionBarOverlayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/decor_content_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
//源码这里是include的,这里为了直观,直接copy进来
<androidx.appcompat.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
<androidx.appcompat.widget.ActionBarContainer
android:id="@+id/action_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
style="?attr/actionBarStyle"
android:touchscreenBlocksFocus="true"
android:keyboardNavigationCluster="true"
android:gravity="top">
<androidx.appcompat.widget.Toolbar
android:id="@+id/action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationContentDescription="@string/abc_action_bar_up_description"
style="?attr/toolbarStyle"/>
<androidx.appcompat.widget.ActionBarContextView
android:id="@+id/action_context_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:theme="?attr/actionBarTheme"
style="?attr/actionModeStyle"/>
</androidx.appcompat.widget.ActionBarContainer>
</androidx.appcompat.widget.ActionBarOverlayLayout>
这个布局默认给我们加了Toolbar相关的属性,是我们通过Theme设置或者requestFeture来设置的,
注意其中的id=action_bar_activity_content
的ContentFrameLayout,后面会有使用到,那这个subDecor又是怎样和DecorView关联起来的呢?接着往下看
ViewGroup subDecor = null;
if (!mWindowNoTitle) {
if (mIsFloating) {
。。。
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
} else {
。。。
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
}
} else {
...
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
这里有去判断subDecor的生成情况,如果是null则会抛出一个异常,这个异常我们应该似曾相识,比如我们项目里面的Activiy继承自AppcompatActivity,但是又没有使用对应的Appcompat下的主题,就会报这个错误
我们接着来看DecorView
和subDecor
关联的点
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
mWindow.setContentView(subDecor);
这里发现会先找出一个ContentFrameLayout,就是我们上面提到的这个id为action_bar_activity_content的布局,然后又将DecorView中将ContentParent找了出来,然后看下面代码
if (windowContentView != null) {
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
这段代码主要做的事是,将ContentParent中的子View都迁移到我们上面从subDecor中找出的ContentFrameLayout中,然后将当前ContentFrameLayout设置为android.R.id.content新的id,之前的ContentParent设置id为NO_ID,然后最后调用mWindow.setContentView(subDecor);
,将subDecor add到之前的ContentParent。秒呀,来了个乾坤大挪移,完成了View的包装和替换,最终我们的Activity的页面视图结构会变成这样
至此,我们的setContentView的流程分析就结束了,从源码中我们可以学习很多知识点,如代理模式,如AppCompatActivity中的兼容替换逻辑,如View的容器化模式等等,理解这些,都可以让我们的编程效率得到提升