0%

Activity 为什么能显示 UI

前言

Activity 作为 Android 中承载 UI 显示的组件,背后的原理是什么呢?本文简单窥探一下。

本文基于 Android-SDK 30 做源码分析

Activity 和 UI 的关系

在 Android 中,调用 startActivity(Intent) 方法就可以打开一个新的页面。

1
2
3
4
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

我们知道 Activity 启动后在其 onCreate 方法,通过开发者主动调用 setContentView 方法,会加载和显示在 activity_main.xml 中定义的 View 组件们。那么,如果我们不调用这个方法会怎么样呢?会正常打开一个新的页面吗?

简单试一下,其实是可以的。

  • 调用 setContentView 后 Activity UI 及 LayoutInspect 结果
  • 不调用 setContentView 后 Activity UI 及 LayoutInspect 结果

可以看到两种场景相差的恰好就是 activity_main.xml 当中的内容。剩下的都是两种场景下公共的部分,比如 DecorView ,StatusBar , NavigationBar 等。那么这些 View 又是怎么如何被管理和添加到当前页面的。再有,一个 Activity 呈现的一个页面,到底是什么? 严格从面向对象的角度出发,他到底是哪个类的实例?一个普通的 Java 类怎么就可以变成花花绿绿的东西,可以显示在屏幕上的?

如果你思考过以上问题,可能多多少少看过一些相关的介绍。当我们调用 startActivity(Intent) 方法启动一个 Activity 的时候,最终 AMS 通过一系列复杂的运作,会回到 ActivityThread 当中执行 performLaunchActivity() 方法,在其中会创建 activity 的实例,并调用 Activity.attach 方法,实现创建 Window 及 View 添加和渲染的一系列操作。

通过以上,我们也可以知道,Activity 类其实并不是负责进行页面渲染的类,他更多的功能是 Android 系统提供给应用开发者方便进行页面生命周期管理和 UI 实现的模板类。当然,Activity 自身涉及到的设计模式也有很多,从不同的角度出发会有不同的答案。

下面就来看看 Android 中 Window 到底是什么?

WindowManager & View

Window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private WindowManager mWindowManager;
@UnsupportedAppUsage
private IBinder mAppToken;
}

Window 这个抽象类定义了上层 window 的基础及一些行为策略。他的唯一实现是 PhoneWindow。

Window 有两个特别重要的或者说常见的属性 WindowManager 和 IBinder 。尤其是当我们在日常开发中错误的操作 Window 时(比如用 application 的 context 调用 startActivity 或进行打开 AlertDialog 的操作)经常会看到 token 相关的报错信息。下面重点说一下 WindowManager 。

WindowManager

首先看看 WindowManager 的基类 ViewManager

ViewManager

1
2
3
4
5
6
7
8
9
/** Interface to let you add and remove child views to an Activity. To get an instance
* of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
*/
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

这里关于这个 ViewManager 的注释很有意思,他是专门用于给 Activity 添加和移除 View 的接口。 注意这里的 LayoutParamas 是 ViewGroup 内定义的。
我们通过调用 getSystemService(Context.WINDOW_SERVICE) 就可以获取到他的实例,当然他本身是个接口,他的实例一般都是用他的子类 WindowManager。

LayoutParamas

1
2
3
4
5
6
7
8
9
10
11
12
/**
* The interface that apps use to talk to the window manager.
* </p><p>
* Each window manager instance is bound to a particular {@link Display}.
*/
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {

public void removeViewImmediate(View view);

public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {}
}

WindowManger 继承 ViewManager 后,添加了键盘弹出收起及截屏的相关接口。最重要的是定义了内部类 LayoutParams 以规范对 WindowManger 进行 add 和 update 的操作时的边界。

有了 WindowManger 我们就可以把一个 View 添加到屏幕上了,只要添加的规则符合 WindowManager.LayoutParams 中定义参数即可。这里及不具体操作了,类似的实现比如屏幕上的悬浮球,悬浮弹窗之类的实现就太多了,具体参考Window/WindowManager 不可不知之事

从以上定义可以看到,我们具体操作的还是 View,Window 不仅是一个抽象类,更是一个抽象的概念。我们添加到屏幕上或更新的东西并不是 Window ,而是更加可以具体化的 View 组件。

这就和我们平时说吃饭是一个道理,虽说是吃饭,但吃的并不是叫做饭的东西,因为实际上在任何一个菜谱不会有一个叫做饭的菜,我们吃的是具体的一道菜,比如宫保鸡丁盖饭或牛肉面。

WindowManagerImpl

WindowManger 依然是抽象的,我们需要看他的具体实现 WindowManagerImpl。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* Provides low-level communication with the system window manager for
* operations that are bound to a particular context, display or parent window.
* Instances of this object are sensitive to the compatibility info associated
* with the running application.
*
* This object implements the {@link ViewManager} interface,
* allowing you to add any View subclass as a top-level window on the screen.
* Additional window manager specific layout parameters are defined for
* control over how windows are displayed. It also implements the {@link WindowManager}
* interface, allowing you to control the displays attached to the device.
*
* <p>Applications will not normally use WindowManager directly, instead relying
* on the higher-level facilities in {@link android.app.Activity} and
* {@link android.app.Dialog}.
*
* <p>Even for low-level window manager access, it is almost never correct to use
* this class. For example, {@link android.app.Activity#getWindowManager}
* provides a window manager for adding windows that are associated with that
* activity -- the window manager will not normally allow you to add arbitrary
* windows that are not associated with an activity.
*
* @see WindowManager
* @see WindowManagerGlobal
* @hide
*/
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

private final Window mParentWindow;

private IBinder mDefaultToken;

public WindowManagerImpl(Context context) {
this(context, null);
}

private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}

public WindowManagerImpl createPresentationWindowManager(Context displayContext) {
return new WindowManagerImpl(displayContext, mParentWindow);
}

/**
* Sets the window token to assign when none is specified by the client or
* available from the parent window.
*
* @param token The default token to assign.
*/
public void setDefaultToken(IBinder token) {
mDefaultToken = token;
}

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
//...
}

这个类的注释很清楚的表达了以下几点

  • 这个类是在更低一层负责和系统的 WindowManagerService 进行通讯,在客户端这个要进行通讯的可能是一个具体的 Context 、Display 或一个存在 Window.
    • 这一点从其构造函数也可以看到,mParentWindow 是可能为 null 的
  • 按照设计模式面向接口编程的理念,上层业务只会接触到 WindowManger ,而具体的操作由他来实现。

但是,具体的操作依旧不是由 WindowManagerImpl 来实现,而是统一桥接到了 WindowManagerGlobal。

WindowManagerGlobal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Provides low-level communication with the system window manager for
* operations that are not associated with any particular context.
*
* This class is only used internally to implement global functions where
* the caller already knows the display and relevant compatibility information
* for the operation. For most purposes, you should use {@link WindowManager} instead
* since it is bound to a context.
*
* @see WindowManagerImpl
* @hide
*/
public final class WindowManagerGlobal {

@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
}

可以对比一下这里的注释和上面 WindowManagerImpl 的注释,同样 low-level 的实现,WindowManagerGlobal 可以说是更加的通用。从命名及之前在 WindowManagerImpl 的初始化可以看到,WindowManagerGlobal 是全局单例的设计模式。因此,可以说他是整个 App 负责进行 window 添加、更新、删除的大管家。

关于 WindowManagerGlobal 内部 view 具体的操作,可以参考 《Android 开发艺术探索》一书了解(这部分内容还是讲的很清楚的,虽然是很久之前出的书,这部分的逻辑其实也没怎么变化,还是值得参考的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {}
}

最后,在沿着最后的逻辑,看看大名鼎鼎的 ViewRootImpl

ViewRootImpl

ViewParent
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Defines the responsibilities for a class that will be a parent of a View.
* This is the API that a view sees when it wants to interact with its parent.
*
*/
public interface ViewParent {
/**
* Called when something has changed which has invalidated the layout of a
* child of this view parent. This will schedule a layout pass of the view
* tree.
*/
public void requestLayout();
}

ViewRootImpl.java

1
2
3
4
5
6
7
8
/**
* The top of a view hierarchy, implementing the needed protocol between View
* and the WindowManager. This is for the most part an internal implementation
* detail of {@link WindowManagerGlobal}.
*/
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {}
  • setView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();

try {
//...
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
setFrame(mTmpFrame);
} catch (RemoteException e) {}
}

到这里就变成了一次跨进程通信,由系统的 WindowManagerService 进行后续操作的流程了。

  • requestLayout
1
2
3
4
5
6
7
8
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

类结构关系

下面通过 UML 图对上文中提到的类总结一下

上面的图简单梳理一下涉及到显示一个页面或者说 Window 的类,以及他们之间的关系。

Activity 显示流程

回到一开始我们的例子,一个 Activity 是如何变成一个可以显示的页面的呢 ?

在 Activity 的启动过程中,我们知道在 ActivityThread 的 performLaunchActivity 方法中,在创建 activity 的实例后会调用其 attach 方法。

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
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// ..
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);

} catch (Exception e) {

}

try {
if (activity != null) {

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);

} catch (SuperNotCalledException e) {}

return activity;
}
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
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) {

attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);

mUiThread = Thread.currentThread();

mMainThread = aThread;
mToken = token;

mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
}

可以看到,在 attach 方法中会创建 PhoneWindow 的实例,同时调用 setWindowManager 。注意,这个 attach 的调用时机非常的早,甚至在我们非常熟悉的 onCreate 之前,因此在这个阶段,Activity 只是创建了一个 Window 并且这个 Window 绑定了 WindowManger .

按照 Activity 的生命周期方法,在 ActivityThread 的 handleResumeActivity 方法中会调用如下代码

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
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {

// 获取 ActivityClientRecord
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

final Activity a = r.activity;

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();
// 赋值 mDecor 为 decor
a.mDecor = decor;


} else if (!willBeVisible) {

}


if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {

// 调用 Activity 的 makeVisible 方法
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
}

即 Activity 的 makeVisible() 方法

1
2
3
4
5
6
7
8
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

可以看到 最终是将 mDecor 添加到了 WindowManager 当中。从前面的 onResume 的中的代码我们也可以看到 这里的 mDecor 其实就是 activity.window.getDecorView() 。

getDecorView 在 Window 中是一个抽象方法,我们可以在其实现类 PhoneWindow 中找到具体实现。

1
2
3
4
5
6
7
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}

看一下 installDecor()

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
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
// ...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);

}
}

protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

在 installDecor 方法中,会生成 DecorView 并赋值给 mDecor,同时会根据 mDecor 初始化 mContentParent 。

查看源码可知,DecorView 只是一个 FrameLayout 。那么我们在 setContentView 中设置的内容又是怎么和 DecorView 关联的呢?

1
2
3
4
5
6
7
8
@Override
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();
}

setContentView 有多个重载方法,但是每个重载方法里都会调用 ensureSubDecor() 的方法。

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
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
// 核心实现还是 createSubDecor
mSubDecor = createSubDecor();
mSubDecorInstalled = true;

}
}

private ViewGroup createSubDecor() {

// 1 , 获取当前系统 Theme ,检查 theme 的合法性并获取一些
// 配置信息
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

// Now let's make sure that the Window has installed its decor by retrieving it
// 2.检查 window 是否已经创建 ,mWindow 赋值
ensureWindow();
// 3. 调用上面说的 getDecorView 创建 DecorView
mWindow.getDecorView();

final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;


// 4. 根据 theme 配置信息,创建 subDecor 及

// subDecor 就是 DecorView 直接子 View,一般会包含 、、/// titleBar 等信息
// Now set the Window's content view with the decor
// 5. 将 subDecor 添加到 mWindow 中,准确说是添加到
// DecorView 中。
mWindow.setContentView(subDecor);

contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
@Override
public void onAttachedFromWindow() {}

@Override
public void onDetachedFromWindow() {
dismissPopups();
}
});

return subDecor;
}

在 PhoneWindow 的 setContentView 方法中最终会将 传入的参数也就是这里的 subDecor 添加到之前创建的 mContentParent 中。至此,DecorView 中已经包含了我们通过 setContentView 设置的内容。按照上面的 onResume 的逻辑,当 DecorView 显示的时候,我们添加的 View 也就显示了。

最后,再来看一个问题,当我们没有调用 setContentView 的时候,为什么也能显示一个正常的窗口呢?这个其实也很简单了,在代码了打个断点就可以发现,除了上面提到的 setContentView 会调用 ensureSubDecor 之外,

1
2
3
4
public void onPostCreate(Bundle savedInstanceState) {
// Make sure that the sub decor is installed
ensureSubDecor();
}

onPostCreate 也会调用这个方法。也就是说,要么我们在 onCreate 中调用,要么在系统会主动在 onPostCreate 中调用。总之,最后一定会创建 DecorView ,并把一个根据 Activity theme 创建出来的 subDecor 添加到 DecorView 当中。

以上就是 Activity 中 UI 显示的梳理。

参考文档

加个鸡腿呗.