oynix

于无声处听惊雷,于无色处见繁花

Activity、Window和DecorView的关系

在Android的视图框架体系,主要由这三者组成。

前言

我们在实际开发中,接触的最多的是Activity,至于Window和DecorView遇到的时候便会相对少一些。虽然不常用,但如果能厘清它们之间的关系,能帮助我们更好的理解Android系统的框架体系设计。

Window

从字面上看,一个Window就是一个窗口,对应的类型有多种,按照层级分为3种:Activity的Window、子Window和系统Window。

Window是一个抽象类,它有且仅有一个实现类,即常说的PhoneWindow,所以,所用到的Window实例,实际上类型都是PhoneWindow。

DecorView

DecorView继承自FrameLayout,扩展了一些自己的功能方法。默认情况下,DecorView会向自己内部添加一个LinearLayout为根节点的layout文件,这个LinearLayout有两个子View,一个是ActionBar,一个是ID为@android:id/content的FrameLayout,我们在Activity里调用setContentView时,就是把我们写的布局文件,作为子View添加到这个FrameLayout下

1
2
3
4
DecorView
|--LinearLayout
|--ActionBar
|--FrameLayout

三者关系

Activity中持有一个Window的实例,在Activity.onAttach中被初始化;Window中持有一个DecorView实例,DecorView就是我们所看到的视图的根视图

1
2
3
4
5
6
Activity
|--PhoneWindow
|--DecorView
|--LinearView
|--ActionBar
|--FrameLayout

在这个视图框架中,Activity处在最外层的位置,表面上,我们的很多交互都是在和Activity进行,但Activity内部会调用Window,Window内部会调用DecorView,最终完成我们的调用。

一个页面的显示过程

众所周知,要在Activity里显示一个页面,通常是在onCreate回调里调用setContentView方法,下面来看一看从设置,到最终显示出来,都经过了哪些流程

  1. 首先,在Activity.setContentView里没有做什么事情,而是直接将调用转给了PhoneWindow

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Activity.java
    public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
    }

    public Window getWindow() {
    return mWindow;
    }
  2. mWindow即为PhoneWindow的实例,它是在Activity.attach方法里初始化的,attach方法的调用是在ActivityThread创建Activity时调用,要早于onCreate,所以在onCreate被调用时,mWindow已经初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // ActivityThread.java
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 1. 创建Activity
    Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    // 2. 调用Activity.attach
    activity.attach(appContext, this, getInstrumentation(), r.token,
    r.ident, app, r.intent, r.activityInfo, title, r.parent,
    r.embeddedID, r.lastNonConfigurationInstances, config,
    r.referrer, r.voiceInteractor, window, r.configCallback,
    r.assistToken);
    // 3. 调用Activity.onCreate
    mInstrumentation.callActivityOnCreate(activity, r.state);
    }

    // Activity.java
    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,
    Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
    // 用PhoneWindow初始化mWindow变量
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(mWindowControllerCallback);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    }
  3. 回到PhoneWindow,在Activity里调用setContentView后,会调用PhoneWindow的setContentView方法,根据上面的介绍可以知道,PhoneWindow中有类型为DecorView的变量,名字为mDecor,我们自己写的页面最终会最为子View添加到DecorView下的LinearLayout下的ID为@android:id/content的FrameLayout里面。

在PhoneWindow里,这个FrameLayout用变量mContentParent保存,所以在PhoneWindow的setContentView里,会先检查mContentParent是都已经初始化,如果没有初始化,则去检查mDecor是否初始化,因为mContentParent是mDecor中的元素,有了mDecor才能初始化mContentParent。

如果mContentParent存在,则会将我们设置的view添加为它的子view。

在官方的设计里,根视图DecorView是由两部分组成的,一个是上面的ActionBar,一个是中间的内容区Content,即mContentParent,所以,我们setContentView是在设置中间内容区的内容。但有时我们不需要ActionBar,会在主题Theme里使用NO_ACTION_BAR的,也可能会有其他的设定,比如需要ToolBar等。

系统中有一些默认的layout文件,在初始化mContentParent时,会根据我们设定的不同的Theme,以及不同的FEATURE,来选对应的layout文件,加载到mDecor中,一般来说这些布局文件中会有一个ID为@android:id/content的FrameLayout,也就是mContentParent。

但是,在实际情况中,还会有一些复杂的情况,比如设置了转场动画等等,以及现在的AppCompatActivity,为了兼容新版SDK的一些功能设计,在DecorView的基础上添加了同样设计的SubDecorView,用SubDecorView来填充DecorView的mContentParent,我们通过setContentView设置的View最终会填充到SubDecorView的mContentParent中,具体这里就不做展开说明了。

下面看看这个过程中对应的一些源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// PhoneWindow.java
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) {
// 1. 如果为空,则去初始化DocerView,同时也会初始化mContentParent
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
// 如果有转场动画,则通过Scene来辅助,没有则将view添加到mContentParent中
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;
}

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 先生成DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 再从mDecor中找到mContentParent,这个方法下面单独说
mContentParent = generateLayout(mDecor);
}
}

// 创建DocorView
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}

mContentParent是在generateLayout方法中创建的,这个方法很长,足足写了三百多行。其中大致做了这几件事:

第一步,获取WindowStyle,类型是TypedArray,这个和我们给Activity设置的Theme中的属性是相对应的,在这里通过R.styleable.Window_来读取,如果写过自定义控件的话,对这种读取属性的方式一定不陌生

第二步,将其中设定的值取出,一部分直接设置给mDecor,如状态栏、导航栏;一部分赋值给PhoneWindow内的成员变量,如mFixedWidthMinor、mFixedWidthMajor;一部分通过setFlags设置给Window中类型为WindowManager.LayoutParams的变量mWindowAttributes,并刷新Window显示,如FLAG_FULLSCREEN,还有一部分直接赋值;一部分通过requestFeature设置给Window的变量mFeatures,如FEATURE_NO_TITLE,这个requestFeature就是我们为修改Activity一些特性,而在setContentView前调用的那个方法,可见系统也是通过这个方法,将我们在主题中设置的参数取出并应用

第三步,根据设定的features,从系统中已经写好的layout布局文件中,找到一个合适的,这些文件可以这个目录下看到,sdk/platforms/android-version/data/res/layout

第四步,加载布局文件,将其添加到mDecor中

第五步,通过@android:id/content这个ID,找到ViewGroup,这便是mContentParent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
// 第一步,获取包含主题属性的TypedArray
TypedArray a = getWindowStyle();

// 第二步,取值,并应用
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
& (~getForcedWindowFlags()));
}
...

// 第三步,根据features,找layout文件
int layoutResource;
int features = getLocalFeatures();
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;
}
removeFeature(FEATURE_ACTION_BAR);
}
...

// 第四步,加载layout文件到mDecor
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

...
// 第五步,初始化mContentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}

至此,PhoneWindow中的setContentView已经走完了,mDecor、mWindow也已经初始化,我们设置的View也添加到了mDecor的content中,但是还没有显示出来,具体让页面内容显示出来,是在resume阶段。

  1. setContentView说完,现在开始说resume。resume是在ActivityThread中的handleResumeActivity方法中回调的,handleResumeActivity里面大致做了这几件事:调用Activity的onResume回调方法、将Activity的Window中的DecorView添加到WindowManager,以及显示Activity的DecorView
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // ActivityThread.java
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
    String reason) {
    // 调用Activity.onResume方法
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

    // 得到DecorView
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    if (a.mVisibleFromClient) {
    if (!a.mWindowAdded) {
    a.mWindowAdded = true;
    // 将decorView加到WindowManager
    wm.addView(decor, l);
    } else {
    a.onWindowAttributesChanged(l);
    }

    // 调用Activity.makeVisible方法,让其显示,此时便看到了页面
    if (r.activity.mVisibleFromClient) {
    r.activity.makeVisible();
    }
    }

    // Activity.java
    void makeVisible() {
    if (!mWindowAdded) {
    ViewManager wm = getWindowManager();
    wm.addView(mDecor, getWindow().getAttributes());
    mWindowAdded = true;
    }
    // 让DecorView可见
    mDecor.setVisibility(View.VISIBLE);
    }
    至此,页面进入可见状态。

总结

布局文件在调用setContentView后,会在PhoneWindow中初始化DecorView,找到DecorView中ID为@android:id/content的ViewGroup,然后将我们设置的View添加到其中,最终在RESUME阶段,会将DecorView添加到WindowManager中,并设置其可见。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2021/09/9fd8392c2130/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道