Android UI 窗口体系 —— 源码阅读

来源:互联网 发布:sql query language 编辑:程序博客网 时间:2024/06/09 13:42

前言:

其实这篇博客是从 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 关联之前需要先初始化//一个 systemLayoutSystemLayout 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管理系统

此致,敬礼!

0 0
原创粉丝点击