Window机制分析

前言

上一篇博客中,我们分析了 Activity 的启动流程,在我看来,Activity 其实只是一个有生命周期的组件,用户看到的其实是显示在屏幕上的 View,而 View 的显示其实和 Activity 是没有直接关系的,具体原因我们下面来慢慢分析。

setContentView()

我们在创建 Activity 之后通常会在它的 onCreate() 方法中调用 setContentView() 方法,这个方法究竟干了什么,这个方法和 Window 是否有关系呢?下面我们来一探究竟:

1
2
3
4
5
6

// Activity.java
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

可以看到,在 Activity 的 setContentView() 方法调用了 Window 的 setContentView() 方法,而 Window 的唯一实现类就是 PhoneWindow,我们来看看 PhoneWindow 的 setContentView() 方法是怎么实现的,代码如下:

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

// 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) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

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

上面的 setContentView() 首先会判断 mContentParent 是否为 null,第一次调用 mContentParent 肯定为 null, 所以会调用 installDecor() 这个方法,通过方法的名称,我们可以判断出这个方法是用来初始化 DecorView 的,DecorView 其实是一个继承自 FrameLayout 的一个特殊的 ViewGroup,具体创建 DecorView 的过程,就在 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
32
33
34
35
36
37
38
39

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

protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

分析 installDecor() 方法,首先调用了 generateDecor() 方法来创建 DecorView 对象,然后在又调用了 generateLayout() 方法,在这个方法中根据一些属性来设置 DecorView,因为里面代码过长,这里就不升入探讨。

回到上面的 setContentView(),当 DecorView 初始化完成之后,会通过 LayoutInflater 的 inflate() 放将我们传入的 layoutResID 添加到 mContentParent 中,这也是这个方法的主要做用。

执行到这里,DecorView 已经创建完毕,并且我们的布局文件已经转换成 View 对象并且添加到 DecorView 中,但是现在 DecorView 并没有 Window 有任何的关系,因为我们调用 setContentView() 的时候,往往是在 onCreate() 方法中,而 onCreate() 被调用的时候,Activity 其实只是刚刚创建,对于用户来说还是不可见的,只有当执行到 onResume() 的时候,用户才能看到我们的 Activity,那么系统应该是在 onResume() 中将 DecorView 和 Window 关联起来的,具体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

// ActivityThread.java
final void handleResumeActivity() {
// ...
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
// ...
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
// ...
}

// Activity.java
void makeVisible() {
if (!mWindowAdded) {
// 调用 WindowManager 的 addView() 方法
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

可以看到,在 Activity 的 makeVisible() 方法中调用了 WindowManager 的 addView() 方法,将 DecorView 添加到 Window 中,但是 WindowManager 只是一个接口,具体的实现是 WindowManagerImpl,而 WindowManagerImpl 中只是做了一下中转,真正的实现在 WindowManagerGlobal 类中,具体代码如下:

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

// 实现 WindowManager 接口
public final class WindowManagerImpl implements WindowManager {

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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


// WindowManagerGlabal.java
public void addView() {
// ...
ViewRootImpl root;
View panelParentView = null;
// ...
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);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
// ...
}

在 WindowManagerGlobal 的 addView() 方法中,首先创建了 ViewRootImpl 对象,这个对象很关键,后面我们分析到,回到上面的 addView() 方法,ViewRootImpl 对象创建完成之后,会调用它的 setView() 方法,而这个 setView() 中会与 WindowManagerService 进行通信,到这里添加 Window 的操作就交由系统的 WindowManagerService 了。

通过上面的分析,我们大致的知道了 setContentView() 的作用以及 ViewRootImpl 的作用,接下来我们通过分析系统的其他几个特性来加深对 Window 的理解。

View 的更新

说到 View 的更新,第一时间想到的应该就是 measure(),layout() 和 draw() 这三个步骤,而这三个不知道是由谁类调用的呢?下面我们就来分析一下:

回到 WindowManagerGlobal 的 addView() 方法中,在 addView() 方法中调用了 ViewRootImpl 的 setView() 方法,在 setView() 中又调用了 requestLayout() 这个方法,哦? 是不是很熟悉,平时我们在自定义 ViewGroup 的时候,就是通过调用这个方法来实现更新界面的,为什么这个方法会出现在 ViewRootImpl 中我们后面在分析,在 requestLayout() 这个方法中调用了一个名为 scheduleTraversals(),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

重点看 scheduleTraversals() 方法中的 postCallback,那个 post 的 runnable 其实就是发起整个 ui 刷新的地方,代码如下:

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

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}

//画重点
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

重点看 performTraversals() 这个方法,这个方法巨长无比,但是核心方法就是三个,具体如下:

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

private void performTraversals() {

//伪代码
performMeasure();
performLayout();
performDraw();

}


// measure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

// layout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;

final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// ...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}

// draw
private void performDraw() {

//伪代码
mView.draw()

}

上面那 mView 其实就是 DecorView,因为这个 mView 是在 setView() 方法被赋值的,其值为 WindowManagerGlobal 的 addView() 方法中的 DecorView。

相信通过上面的伪代码就能明白整个 UI 刷新是怎么发起的了,其实在内部还用很多细节,比如会在 perform 打头的方法中调用 TreeObserver 中的相应方法。

findViewById()

说到 Android 上面代码写的最多,当属 findViewById(),但是很少有人这个方法背后的原理,接下来我们就来一探究竟。源代码如下:

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

// Activity.java
public <T extends View> T findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}

// PhoneWindow.java
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}

// DecorView 继承自 FrameLayout
// FrameLayout 继承自 ViewGroup
// ViewGroup 继承自 View
// 所以我们只用看 View 和 ViewGroup 的源码

// View.java
public final <T extends View> T findViewById(@IdRes int id) {
//过滤掉没用的 ID
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}


protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
return null;
}

// ViewGroup.java
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}

final View[] where = mChildren;
final int len = mChildrenCount;

for (int i = 0; i < len; i++) {
View v = where[i];

if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);

if (v != null) {
return (T) v;
}
}
}

return null;
}

可以看到,在 ViewGroup 中只是重写了 findViewTraversal() 这个方法,在这个方法中循环调用子 View 的 findViewById() 方法,整个过程就是一个对树形结构的遍历。

说几句题外话,平时我们都会看到面试官问这么优化 UI 的性能,其实通过阅读 Android 的源码这个问题就迎刃而解了,比如我们可以通过减少布局的层级来减少 mesure,layout,draw 这三个方法的执行时间,并且还可以减少 findViewBy() 的执行时间,其次当我们调用 findViewById() 之后最好把找到的 View 对象保存起来,这样后面在用的的时候,就不需要在调用 findViewById() 了,因为 findViewById() 是一个耗时的过程,消耗的时间和布局的深度有这直接的关系。

这里既然看到了 View 的 findViewById() 方法,我们顺便看看 View 的 requestLayout() 方法,源码如下:

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

public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

在 requestLayout() 方法的开头,会判断当前的 ViewRootImpl 是否正在 layout,如果正在 layout 就直接 return,接着下面会调用 mParent 的 requestLayout() 方法,那么就会有一个问题,当我们调用一个 View 的 requestLayout() 方法之后,最终会走到顶级 View 的 requestLayout 方法中,很明显这个顶层的 View 绝对要重写 requestLayout() 这个方法,而这个顶层的 View 其实就是 ViewRootImpl,换句话说 ViewRootImpl 本身并不是 View,但是所有的 View 的操作都会由它来处理。

在 ViewRootImpl 的 setView() 方法中会调用 DecorView 的 assignParent() 方法,并把自身传进去,而 ViewRootImpl 是实现了 ViewParent 这个接口的,源码如下:

1
2
3
4
5
6
7
8
9
10
11

void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}

在 assignParent() 方法中,会将 mParent 指向参数 parent,这样 requestLayout() 最终的处理逻辑就执行到了 ViewRootImpl 中,在上面代码中还看到了一个很熟悉的一个异常,那就是我们如果企图将一个已经添加到 ViewGroup 中的 View 在添加到另外一个 ViewGroup 就会触发这个异常。

Dialog

上面说完 requestLayout(),下面在来说说 Dialog,Dialog 是使用 Window 来实现的,具体实现原理,我们下面来慢慢分析,源码如下:

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

// Dialog 的构造函数
public Dialog(@NonNull Context context) {
this(context, 0, true);
}

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}

mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);

mListenersHandler = new ListenersHandler(this);
}

可以看到,在 Dialog 的构造函数中,首先是使用 getSystemService() 方法获取到 WindowManager,然后又 new 了一个 PhoneWindow 对象,然后设置一系列的 CallBack。

这里创建 Window 的方法和 Activity 创建 Window 的方法一模一样,具体可查看 Activity 的 attach() 方法。

接下来在来看看 Dialog 的 setContentView() 方法,源码如下:

1
2
3
4

public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}

可以看到,Dialog 的 setContentView() 和 Activity 的 setContentView() 基本一致,都是调用了 Window 的 setContentView() 方法。

紧接着我们在来看看 Dialog 的 show() 方法,源码按如下:

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

public void show() {
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}

mCanceled = false;

if (!mCreated) {
dispatchOnCreate(null);
} else {
// Fill the DecorView in on any configuration changes that
// may have occured while it was removed from the WindowManager.
final Configuration config = mContext.getResources().getConfiguration();
mWindow.getDecorView().dispatchConfigurationChanged(config);
}

onStart();
mDecor = mWindow.getDecorView();

if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
final ApplicationInfo info = mContext.getApplicationInfo();
mWindow.setDefaultIcon(info.icon);
mWindow.setDefaultLogo(info.logo);
mActionBar = new WindowDecorActionBar(this);
}

WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}

mWindowManager.addView(mDecor, l);
mShowing = true;

sendShowMessage();
}

在 Dialog 的 show() 方法中,首先会判断当前 Dialog 是否被显示,如果已经显示就只会调用 mDecor 的 setVisibility() 方法,因为 Dialog 除了有 show() 方法还有 hide() 方法。

接这会判断当前 Dialog 是否调用过 onCreate() 方法,注意这个 onCreate() 是 Dialog 的而不是 Activity 的,虽然和 Activity 的 onCreate() 长得很像,如果 onCreate() 已经被调用过了,那么就会触发 onConfigurationChanged() 这个方法,这个方法是每个 View 都能重写的,然后会调用 Dialog 的 onStart() 方法。

最后后调用 WindowManager 的 addView() 方法把 mDecor 这个 DecorView 添加到系统中去。执行到这里 Dialog 就显示到屏幕上了,是不是感觉流程和 Activity 很相似,其实 Dialog 给我的感觉就像是一个简化版本的 Activity。

全局 Dialog

使用过 Dialog 的同学应该知道,想要显示一个 Dialog 必须要使用 Activity 的实例作为构造函数中 context 的参数,如果不使用 Activity 做为 context 的参数,就会出现下面的异常:

BadTokenException

触发这个异常的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

fun onClick(view: View){
//这里使用的是 getApplicationContent() 作为参数传入
AlertDialog.Builder(applicationContext)
.setTitle("测试")
.setMessage("测试")
.show()
}
}

触发的异常的原因也很加简单,系统为了防止一些流氓 APP 随意将它们的 View 显示到屏幕上(注意说所的显示到屏幕上指的是使用 WindowManager.addView() 方法添加的 View),设定了一套 Token 机制,这个 Token 机制主要的作用就是用来限制当一个 Context 对象不是一个 Activity 的时候,即没有 Activity 正在与用户交互的时候,是不允许调用 WindowManager.addView() 方法的,说白了,除了系统进程和当前正在和用户交互的 Activity,其他的进程是无法将 View 添加到屏幕中的。

但是在日常的开发中,总会遇到一些特殊的需求需要 Dialog 随时都能弹出来,这个时候就可以使用全局的 Dialog,具体实现方式如下:

Toast

最后我们来说说 Toast,比起 Dialog,Toast 恐怕比前者的使用场景还要多,有事没事都能弹一个 Toast,登陆成功了弹个 Toast,刷新成功弹个 Toast 等等。今天我们来分析分析 Toast 是怎么实现的,以及怎么来实现一个自定义的 Toast。

Toast 原理

按照惯例,分析原理之前先来看看使用方法,代码如下:

1
2
3

Toast.makeText(context, "我是长 Toast", Toast.LENGTH_LONG).show()
Toast.makeText(context, "我是短 Toast", Toast.LENGTH_SHORT).show()

一行代码,非常的简单,首先是通过 Toast 的静态方法 makeText() 创建一个 Toast 对象,然后在调用它的 show() 方法,我们先从 makeText() 方法看起,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

可以看到在 makeText() 方法中,首先 new 了一个 Toast 对象,然后通过 LayoutInflater 将一个 ID 为 transient_notification 的布局文件解析成一个 View 对象,并且获取其中的 一个 ID 为 message 的 TextView 并调用它的 setText() 方法将我们传入的 content 设置给它,最后设置一下 Toast 对象中的属性并返回。

ID 为 transient_notification 的布局文件内如如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">

<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/bright_foreground_dark"
android:shadowColor="#BB000000"
android:shadowRadius="2.75"
/>

</LinearLayout>

看完 makeText() 紧接着再来看看 Toast 的构造函数,源码如下:

1
2
3
4
5
6
7
8
9

public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}

可以看到在构造函数中,创建了一个类型为 TN 的对象并赋值给 mTN 这个变量,这个 TN 其实使用 Binder,用来处理 Binder 的会调的,后面我们会看到,接下来再来看看 show() 方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

在 show() 方法中,滴啊用了 mTN 的 enqueueToast() 方法,很显然这个方法是一个 IPC 的过程,我们暂且不敢方法做了什么,我们只用看 TN 这个类中回调中干了什么,源码如下:

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

private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

private static final int SHOW = 0;
private static final int HIDE = 1;
private static final int CANCEL = 2;
final Handler mHandler;

int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;


View mView;
View mNextView;
int mDuration;

WindowManager mWM;

String mPackageName;

static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;

TN(String packageName, @Nullable Looper looper) {
// ...
if (looper == null) {

looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
mNextView = null;
break;
}
case CANCEL: {
handleHide();
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}

// ...

}

我们先看 TN 的构造函数,在构造函数中先初始化了 mParams 这个类型为 WindowManager.LayoutParams 的成员变量,让后检查了 looper 这个参数,从这个判断我们可以推断出,如果我们想在子线程中使用 Toast 那么就必须要调用 Looper.prepare() 这个方法,最后创建了一个 Handler 用于将一些代码切换到 UI 线程中。

构造函数差不多就这么多,接着在来看看 show() 和 hide() 方法。源码如下:

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

private static class TN extends ITransientNotification.Stub {

// ...

@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

@Override
public void hide() {
mHandler.obtainMessage(HIDE).sendToTarget();
}

public void cancel() {
mHandler.obtainMessage(CANCEL).sendToTarget();
}

public void handleShow(IBinder windowToken) {
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
mWM.removeView(mView);
}
try {
mWM.addView(mView, mParams);
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}

public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
mWM.removeViewImmediate(mView);
}

mView = null;
}
}
}

首先我们知道,show() 方法和 hide() 方法是一个 IPC 的回调方法,本身是执行在 Binder 的线程池中的,所以需要使用 Handler 来将线程切换到主线程中,看到 Handler 的 handleMessage() 最终会调用 handleShow() 和 handleHidle() 这两个方法。

我们在上面说到过,如果我使用非系统进程或者不是当前位于前台的进程,是无法调用 WindowManager 的 addView() 的,除非我们有一个 WindowToken,很显然这个 Token 通过 show() 这个回调方法返回了。

来看看 handleShow() 方法,在 handleShow() 中会首先获取 WindowManagerService,然后调用它的 addView() 方法,在调用 addView() 之前会把 windowToken 设置给 mParams 的 token 这样就不会报 BadTokenException,当让这不是绝对的,在一些特殊的情况下还是会报错,所以在 addView() 的外层加了个 try catch 并直接把这个异常忽略,这样就可以避免因为一些外界因素导致 APP 主进程的崩溃。

hadnleHide() 就相对简单许多,仅仅只是调用 WindowManagerService 的 removeViewImmediate() 方法。

自定义 Toast

分析完 Toast 的源码,我想对于 Toast 的自定义,应该就简单很多了,关于自定义的 Toast 和 Dialog 主要的区别如下:

Toast 属于系统级别的 Window,不会应为应用切回后台就无法显示,当然,Toast会有一个指定的消失机制。换言之,Toast 可以在没有 Activity 的情况下显示,Dialog 却不行(如果有权限的话 Dialog 也可以不需要 Activity 也能显示)。

下面我们来看看怎么实现一个简单的自定义 Toast,代码如下:

1
2
3
4
5

// MainActivity.kt
val toast = Toast(this)
toast.view = LayoutInflater.from(this).inflate(R.layout.layout_custom_toast,null)
toast.show()

自定义 Toast 其实很简单,核心代码就一行,就是调用 Toast 对象的 setView() 方法,注意这里 Toast 对象要用 new 方法来创建,不能用 makeText() 方法创建。

其实自定义 Toast 意义不是很大,因为 Toast 会有一个自动取消的机制,这个机制主要是为了防止某些垃圾 APP 通过 Toast 弹出强制弹出一些无法取消的垃圾信息,所以我们自定义的 Toast 只能表达一些简短的信息,所以只使用系统提供的 Toast 完全够用。

总结

分析了这么多,终于算是对 Android 的 Window 机制有了一个初步理解,本文主要是通过下面几个方面来了解 Window 机制。

  1. 通过分析 Activity 的 setContentView() 方法,我们知道了,DecorView 和 VieRootImpl 这两个东西,在 setContentView() 方法中主要是创建 DecorView 和 将我们的布局添加到 DecorView 中,而在 Activity 的 onResume() 方法中,会将 DecorView 添加到 Window 中。(这里说添加到 Window 中不准确)。
  1. 通过分析 Dialog 的实现原理,我们知道了,其实 Dialog 是一个小型的 Activity 因为它实现了和 Activity 一样的功能,就是将一个 View 对象显示在屏幕上,和 Activity 的区别在于,Activity 有 DecorView 这个东西,而 Dialog 没有。当然 Activity 还有自己的生命周期方法,这个 Dialog 是没有的,不过我们这里仅仅只讨论 Window 相关的。
  1. 通过分析 Toast 的源码,我们知道了,其实 Toast 只是一个 View,然后通过 WindowManager.addView(),方法将在这个 View 显示到屏幕上,它和 Dialog 的区别就是,Toast 是可以拿到 WindowToken 的,而 Dialog 却需要在 Activity 的基础之上才能拿到 WindowToken。

最后来几张图总结一下 Window 的机制。

Activity 的 setContentView 的流程图:

setContentView()

Dialog() 原理: