前言:
其实这篇博客是从 Android 事件分发机制 中分离出来的,当时在分析事件分发的博文中插播了这篇博客的内容,但是后来发现使得 事件分发 的博文变得臃肿,不够纯粹,于是还是将那部分内容分离了出来。
由来
在事件分发中分析到 PhoneWindow、DecorView 的时候卡住了,那么这些类与事件分发有什么关系呢,要知道,我们需要关心的其实是我们 setContentView(view) 中 view 分发过程,而我的疑惑就在于 PhoneWindow、DecorView 这些类与我们自定义的布局有什么关联呢? 那么就不得不了解下 Android 的窗口机制了。
这篇博客最初始的排版,我先放了一张 Android UI 层次的经典图,后来终于还是决定把这张图放到了博客的最后,我想还是应该从源码的层次来分析,一味地记住结论,那是死记硬背。当然,前面已经说到,这篇博客的出现是为了事件分发而存在的,那么在分析的时候必然会忽略其他一些重要的细节。
思路
从事件分发的分析中,我们知道从 Activity 传递进来的 ev 最终是被 mDecorView 处理了,那么 mDecorView 到底与我们的 view 有什么关系呢,既然我们的 View 是从 setContentView 设置进去的,那么阅读的突破口自然也就是 setContentView 。
读代码
MainActivity.onCreate(…)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
setConentView(…)
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
getDelegate()
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
到这里我们发现在 Activity 将 setContentView 的具体实现交由 AppCompatDelegate 来做了,那么我们来看一看:
AppCompatDelegate.create
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
从上面的代码中可以看出 AppCompatDelegate 中的 Window 对象其实就是 Activity 中初始化的 PhoneWindow 对象。
AppCompatDelegate.careate(… params)
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
AppCompatDelegate 作为一个抽象类,方法的最终的实现是由它的子类来实现的,从最原始的 AppCompatDelegateImplV9 版本阅读:
AppCompatDelegateImplV9.setContentView(int resId)
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
从上面的代码中可以发现我们的 view 最终,被添加到 mSubDecor 的子view — ViewGroup.contentParent 中了。那么换言之,我们从 寻找 ourCustomizedView 与 DecorView 的关系 变成了 寻找 AppCompatDelegate.mSubDecor 与 DecorView 的关系了:
AppCompatDelegateImplV9.ensureSubDecor()
private void ensureSubDecor() {
....
mSubDecor = createSubDecor();
...
}
AppCompatDelegateImplV9.createSubDecor()
private ViewGroup createSubDecor() {
......
// 源码中根据多种参数判断加载不同的内置 layout_resId
subDecor = (ViewGroup) inflater.inflate(
R.layout.resId, null);
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
......
return subDecor;
}
最终 subDecor 终于被我们 Activity 中的 PhoneWindow 调用了 – mWindow.setContentView(subDecor);
mWindow.setContentView(subDecor)
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mWindow.setContentView(View view, ViewGroup.LayoutParams params)
@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();
}
......
mContentParent.addView(view, params);
......
}
从上面的代码中,可以看到 mContentParent.addView(view, params)
。
到此 PhoneWindow 将 mSubDecor 作为子View添加到了 PhoneWindow.mContentParent 中。
那么关系对象再次转换 我们从寻找 AppCompatDelegate.mSubDecor 与 DecorView 的关系 变成了 PhoneWindow.mContentParent 与 DecorView 的关系了,从上面的代码中可以看到 :
if (mContentParent == null)
installDecor();
显然在 installDecor() 这个函数中,肯定将 mContentParent 与 DecorView 做了关联。
PhoneWindow.installDecor()
private void installDecor() {
......
if (mDecor == null) {
mDecor = generateDecor(-1);
......
}
......
if (mContentParent == null)
mContentParent = generateLayout(mDecor);
......
}
在初始化 DecorView 之后,又将 mDecor 作为参数传入了 generateLayout(… params) ,并将返回值赋值给了 PhoneWindow.mContentParent,那么不妨来看看这个函数返回的 View 到底是怎么来的。
PhoneWindow.generateLayout(DecorView decor)
protected ViewGroup generateLayout(DecorView decor) {
......
// Inflate the window decor.
int layoutResource;
......
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
......
return contentParent;
}
在第一次分析到这个函数的时候,纠结了好久,因为从代码表面上看上去 contentParent 与 mDecor 似乎是没有任何联系的,最后终于决定倒着来看代码。
第一步
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//在 Window 类中定义了一个常量值,看注释的意思,每个主要的xml布局文件中都会有这个 id 的存在。
// 那么这个 main layout 到底是什么呢,这个 main layout 和 DecorView 又有什么关系呢
/**
* 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;
可是光有这个依然无法解除我的困惑,我当时还没有想到要进入 findViewById 这个函数去看,然后我依然继续往上看代码:
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
到这里我看到了 布局填充器对象 和 layout 资源
DecorView.onResourcesLoaded(mLayoutInflater, layoutResource)
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
.......
final View root = inflater.inflate(layoutResource, null);
......
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
......
}
看到 addView 我愣了一下,才反应过来 DecorView 它本身就是个 ViewGroup , 也就是说最终真正被添加到 DecorView 中的 View 是 layoutResource 所代码的布局。
最终被添加到 DecorView 中的布局是 layoutResource 所代表的布局,而之前赋值 contentParent 的代码 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)
,这很快就能联想到 ID_ANDROID_CONTENT 这个 id 肯定是和 layoutResource 有关系的,而之前对于 layoutResource 的注释也说到这是所有 main layout 都会存在的id,而在之前的分析中对于 PhoneWindow.generateLayout(DecorView decor)
我省略了赋值 layoutResource 的代码,现在可以贴出来分析下:
PhoneWindow.generateLayout(DecorView decor)
protected ViewGroup generateLayout(DecorView decor) {
......
// 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();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
......
}
对于赋值 layoutResource 的代码非常多,那么我要确定的只是这些布局和 com.android.internal.R.id.content
的关系,那我从下往上贴几个布局出来
例 R.layout.screen_simple
<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>
例 R.layout.screen_simple_overlay_action_mode
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<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" />
<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>
例 R.layout.screen_simple_overlay_action_mode
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<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:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
尽管我只贴出了3个布局的具体xml内容,但是你会发现每一个布局中都会有一个 id 为 content 的 FrameLayout 的控件,layoutResource 作为 R.id.content 的 ViewParent 的存在,其实它的作用就是根据设置的 theme 和 Activity 的窗口类型来选择系统级的父布局。
可是 contentView 是在 PhoneWindow.findViewById 中找到的,可以想象这个 findViewById 肯定和 mDecorView 有所关联,最终我在 Window 的源码中找到了这个函数的实现:
Window.findViewById(id)
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
伪代码流程
到这里,我想所有我们疑惑的关系都已经关联上了,我想用伪代码来描述我们分析的过程的话其实是这样的:
setContentView(costumeView) ->
AppCompatDelegate.mSubDecor.addView(costumeView) ->
// 接下来在将 AppCompatDelegate.mSubDecor 与 PhoneWindow.mContentParent 关联之前需要先初始化
//一个 systemLayout
SystemLayout systemLayout = PhoneWindow.mDecor.mLayoutInflater.inflater(layoutResource) ->
PhoneWindow.mDecore.addView(systemLayout) ->
PhoneWindow.mContentParent = PhoneWindow.mDecore.findViewById(R.id.content) ->
PhoneWindow.mContentParent.addView(AppCompatDelegate.mSubDecor)
经典网图
上面的分析,是我在假装不知道 UI 体系的情况下,根据阅读源码所得到的结论,那么从很多网友的博客中都能看到一张非常经典的层级图:
PhoneWindow
PhoneWindow是Android中的最基本的窗口系统,每个Activity 均会创建一个PhoneWindow对象,是Activity和整个View系统交互的接口。
DecorView
DecorView是当前Activity所有View的祖先,它并不会向用户呈现任何东西,它主要有如下几个功能,可能不全:
A. Dispatch ViewRoot分发来的key、touch、trackball等外部事件;
B. DecorView有一个直接的子View,我们称之为System Layout,这个View是从系统的Layout.xml中解析出的,它包含当前UI的风格,如是否带title、是否带process bar等。可以称这些属性为Window decorations。
C. 作为PhoneWindow与ViewRoot之间的桥梁,ViewRoot通过DecorView设置窗口属性。
System Layout (其实就是 layoutResource 所代表的布局)
目前android根据用户需求预设了几种UI 风格,通过PhoneWindow通过解析预置的layout.xml来获得包含有不同Window decorations的layout,我们称之为System Layout,我们将这个System Layout添加到DecorView中,目前android提供了8种System Layout,如下图。
预设风格可以通过PhoneWindow方法requestFeature()来设置,需要注意的是这个方法需要在setContentView()方法调用之前调用。
Content Parent (在 layoutResource 对应的是 Id 为 content 的 FrameLayout ,在 PhoneWindow 中对应的是 PhoneWindow.mContentParent)
Content Parent这个ViewGroup对象才是真真正正的ContentView的parent,我们的ContentView终于找到了寄主,它其实对应的是System Layout中的id为”content”的一个FrameLayout。这个FrameLayout对象包括的才是我们的Activity的layout(每个System Layout都会有这么一个id为”contenet”的一个FrameLayout)。
Activity Layout
这个ActivityLayout便是我们需要向窗口设置的ContentView,现在我们发现其实它的地位很低,同时这一部分才是和user交互的UI部分,其上的几层并不能响应并完成user输入所期望达到的目的。
最终
这篇博客的初衷就是为了更好的分析事件分发而分离出来的博客,重点在于理清 Android UI 体系中的层级关系,其他的对于我而言都是不关心的。
感谢以下的博文:
Window窗口布局 — DecorView浅析
android的窗口机制分析——UI管理系统
此致,敬礼!