其实这篇博客是从 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管理系统
此致,敬礼!