深入理解WMS(二):Dialog与Toast源码解析(1)

getWindow().setContentView(layoutResID);

initWindowDecorActionBar();

}

从Activity的setContentView方法我们可以清楚的看到,getWindow()返回的实际上是上面创建的PhoneWindow,也就是它会调用PhoneWindow的setContentView,在该方法中会创建DecorView并完成布局视图的填充。下面我们看下PhoneWindow的setContentView的源码。

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

}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

view.setLayoutParams(params);

final Scene newScene = new Scene(mContentParent, view);

transitionTo(newScene);

} else {

mContentParent.addView(view, params);

}

mContentParent.requestApplyInsets();

final Callback cb = getCallback();

if (cb != null && !isDestroyed()) {

cb.onContentChanged();

}

mContentParentExplicitlySet = true;

}

通过上面的源码我们能清楚的看到大概分为以几个步骤:

  1. 如果没有DecorView,则需要创建,否则移除其中的mContentParent中所有的View。

  2. 将View添加到DecorView的mContentParent中。

  3. 回调Activity的onContentChanged方法通知Activity视图已经发生改变。

经过上面几个步骤,DecorView就创建完并初始化成功了。Activity的布局文件也已经成功添加到了DecorView的mContentParent中,但是这个时候DecorView还没有被WindowManager正式添加到Window中。这里需要正确理解Window的概念,Window更多表示的是一种抽象的功能集合,虽然说早在Activity的attach方法中Window就已经被创建了,但是这个时候由于DecorView并没有被WindowManager识别,所以这个时候的Window无法提供具体功能,因为它还无法接收外界的输入信息。在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible(),正是在makeVisible方法中,DecorView真正地完成了添加和显示这两个过程,到这里Activity的视图才能被用户看到。

void makeVisible() {

if (!mWindowAdded) {

ViewManager wm = getWindowManager();

wm.addView(mDecor, getWindow().getAttributes());

mWindowAdded = true;

}

mDecor.setVisibility(View.VISIBLE);

}

2. Dialog中的Window的创建过程

Dialog的Window的创建过程跟Activity的很相似,大体有以下几个步骤。

-1. 创建Window

Dialog的Window的创建同样是PhoneWindow,这个剩下的跟Activity还是很类似的。具体看下下面的源码。

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

}

-2. 初始化DecorView并将Dialog的界面添加到DecorView中

这个过程跟Activity也是类似的,也是通过Window去添加指定的布局。

/**

  • Set the screen content from a layout resource. The resource will be

  • inflated, adding all top-level views to the screen.

  • @param layoutResID Resource ID to be inflated.

*/

public void setContentView(@LayoutRes int layoutResID) {

mWindow.setContentView(layoutResID);

}

-3. 将DecorView添加到Window中并显示

Dialog的show方法中,会通过WindowManager将DecorView添加到Window中,源码如下

mDecor = mWindow.getDecorView();

mWindowManager.addView(mDecor, l);

mShowing = true;

其实从上面的三个步骤能看出,Dialog的Window创建过程跟Activity的很类似,几乎没有多少区别。当Dialog关闭时,会通过WindowManager来移除DecorView。

普通的Dialog有个不同之处,就是必须要使用Activity的Context,如果使用Application的Context会报错。这个地方是因为普通的Dialog需要token,而token一般是Activity才会有,这个时候如果一定要用Application的Context,需要Dialog是系统的Window才行,这就需要一开始设置Window的type,一般选择TYPE_SYSTEM_OVERLAY指定Window的类型为系统Window。

3 Toast的Window创建过程

Toast和Dialog不同,它的工作过程就稍显复杂。首先Toast也是基于Window来实现的,但是由于Toast具有定时取消这一功能,所以系统采用了Handler。在Toast的内部有两类IPC过程,第一类是Toast访问NotificationManagerService,第二类是Notification-ManagerService回调Toast里的TN接口。关于IPC的一些知识,可以移步Android中的IPC方式。为了便于描述,下面将NotificationManagerService简称为NMS。

Toast属于系统Window,它内部的视图由两种方式指定,一种是系统默认的样式,另一种是通过setView方法来指定一个自定义View,不管如何,它们都对应Toast的一个View类型的内部成员mNextView。Toast提供了show和cancel分别用于显示和隐藏Toast,它们的内部是一个IPC过程,下面我们看下show方法跟cancel方法。

/**

  • Show the view for the specified duration.

*/

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

}

}

/**

  • Close the view if it’s showing, or don’t show it if it isn’t showing yet.

  • You do not normally have to call this. Normally view will disappear on its own

  • after the appropriate duration.

*/

public void cancel() {

mTN.cancel();

}

从上面的代码可以看到,显示和隐藏Toast都需要通过NMS来实现,由于NMS运行在系统的进程中,所以只能通过远程调用的方式来显示和隐藏Toast。需要注意的是TN这个类,它是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示或隐藏请求时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。这里的当前线程是指发送Toast请求所在的线程。注意,由于这里使用了Handler,所以这意味着Toast无法在没有Looper的线程中弹出,这是因为Handler需要使用Looper才能完成切换线程的功能.

从上面源码show方法我们可以看到,Toast的显示调用了NMS的enqueueToast方法。enqueueToast方法有三个参数,分别是:pkg当前应用包名、tn远程回调和mDuration显示时长。

enqueueToast首先将Toast请求封装为ToastRecord对象并将其添加到一个名为mToastQueue的队列中。mToastQueue其实是一个ArrayList。对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,这样做是为了防止DOS(DenialofService)。如果不这么做,试想一下,如果我们通过大量的循环去连续弹出Toast,这将会导致其他应用没有机会弹出Toast,那么对于其他应用的Toast请求,系统的行为就是拒绝服务,这就是拒绝服务攻击的含义,这种手段常用于网络攻击中。

// Limit the number of toasts that any given package except the android

// package can enqueue. Prevents DOS attacks and deals with leaks.

if (!isSystemToast) {

int count = 0;

final int N = mToastQueue.size();

for (int i=0; i<N; i++) {

final ToastRecord r = mToastQueue.get(i);

if (r.pkg.equals(pkg)) {

count++;

if (count >= MAX_PACKAGE_NOTIFICATIONS) {

Slog.e(TAG, "Package has already posted " + count

  • " toasts. Not showing more. Package=" + pkg);

return;

}

}

}

}

// If it’s at index 0, it’s the current toast. It doesn’t matter if it’s

// new or just been updated. Call back and tell it to show itself.

// If the callback fails, this will remove it from the list, so don’t

// assume that it’s valid after this.

if (index == 0) {

showNextToastLocked();

}

正常情况下,一个应用不可能达到上限,当ToastRecord被添加到mToastQueue中后,NMS就会通过showNextToastLocked方法来显示当前的Toast。下面的代码很好理解,需要注意的是,Toast的显示是由ToastRecord的callback来完成的,这个callback实际上就是Toast中的TN对象的远程Binder,通过callback来访问TN中的方法是需要跨进程来完成的,最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池中。

@GuardedBy(“mToastQueue”)

void showNextToastLocked() {

ToastRecord record = mToastQueue.get(0);

while (record != null) {

if (DBG) Slog.d(TAG, “Show pkg=” + record.pkg + " callback=" + record.callback);

try {

record.callback.show(record.token);

scheduleTimeoutLocked(record);

return;

} catch (RemoteException e) {

Slog.w(TAG, "Object died trying to show notification " + record.callback

  • " in package " + record.pkg);

// remove it from the list and let the process die

int index = mToastQueue.indexOf(record);

if (index >= 0) {

mToastQueue.remove(index);

}

keepProcessAliveIfNeededLocked(record.pid);

if (mToastQueue.size() > 0) {

record = mToastQueue.get(0);

} else {

record = null;

}

}

}

}

从上面的源码可以看到,Toast显示之后,通过scheduleTimeoutLocked来发送一个延时消息,时长当然是根据一开始设置的时间。具体看下代码:

@GuardedBy(“mToastQueue”)

private void scheduleTimeoutLocked(ToastRecord r)

{

mHandler.removeCallbacksAndMessages®;

Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);

long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;

mHandler.sendMessageDelayed(m, delay);

}

上面LONG_DELAY是3.5s,SHORT_DELAY是2s。延时过后,NMS会通过cancelToastLocked来隐藏Toast并从mToastQueue中移除,我们看下源码就能清楚的了解这个过程,下面是cancelToastLocked方法,可以看到移除Toast之后如果mToastQueue有Toast又调用了showNextToastLocked方法。

@GuardedBy(“mToastQueue”)

void cancelToastLocked(int index) {

ToastRecord record = mToastQueue.get(index);

try {

record.callback.hide();

} catch (RemoteException e) {

Slog.w(TAG, "Object died trying to hide notification " + record.callback

  • " in package " + record.pkg);

// don’t worry about this, we’re about to remove it from

// the list anyway

}

ToastRecord lastToast = mToastQueue.remove(index);

mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

keepProcessAliveIfNeededLocked(record.pid);

if (mToastQueue.size() > 0) {

// Show the next one. If the callback fails, this will remove

// it from the list, so don’t assume that the list hasn’t changed

// after this point.

showNextToastLocked();

}

}

经过上面的分析,我们了解到Toast的显示和隐藏过程实际上是通过Toast中的TN这个类来实现的,它有两个方法show和hide,分别对应Toast的显示和隐藏。由于这两个方法是被NMS以跨进程的方式调用的,因此它们运行在Binder线程池中。为了将执行环境切换到Toast请求所在的线程,在它们的内部使用了Handler,具体看下源码:

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

// Don’t do this in handleHide() because it is also invoked by

// handleShow()

mNextView = null;

break;

}

case CANCEL: {

handleHide();

// Don’t do this in handleHide() because it is also invoked by

// handleShow()

mNextView = null;

try {

getService().cancelToast(mPackageName, TN.this);

} catch (RemoteException e) {

}

break;

}

}

}

};

/**

  • schedule handleShow into the right thread

*/

@Override

public void show(IBinder windowToken) {

if (localLOGV) Log.v(TAG, "SHOW: " + this);

mHandler.obtainMessage(SHOW, windowToken).sendToTarget();

}

/**

  • schedule handleHide into the right thread

*/

@Override

public void hide() {

if (localLOGV) Log.v(TAG, "HIDE: " + this);

mHandler.obtainMessage(HIDE).sendToTarget();

}

上述代码中,mShow和mHide是两个Runnable,它们内部分别调用了handleShow和handleHide方法。由此可见,handleShow和handleHide才是真正完成显示和隐藏Toast的地方。TN的handleShow中会将Toast的视图添加到Window中。代码如下。

// Since the notification manager service cancels the token right

// after it notifies us to cancel the toast there is an inherent

// race and we may attempt to add a window after the token has been

// invalidated. Let us hedge against that.

try {

mWM.addView(mView, mParams);

trySendAccessibilityEvent();

} catch (WindowManager.BadTokenException e) {

总结

Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

上面分享的字节跳动公司2020年的面试真题解析大全,笔者还把一线互联网企业主流面试技术要点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

就先写到这,码字不易,写的很片面不好之处敬请指出,如果觉得有参考价值的朋友也可以关注一下我

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包阅读下载,最后觉得有帮助、有需要的朋友可以点个赞


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
t.

try {

mWM.addView(mView, mParams);

trySendAccessibilityEvent();

} catch (WindowManager.BadTokenException e) {

总结

Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

上面分享的字节跳动公司2020年的面试真题解析大全,笔者还把一线互联网企业主流面试技术要点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

就先写到这,码字不易,写的很片面不好之处敬请指出,如果觉得有参考价值的朋友也可以关注一下我

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包阅读下载,最后觉得有帮助、有需要的朋友可以点个赞

[外链图片转存中…(img-pDWVMlL4-1715337518874)]

[外链图片转存中…(img-c68qF4NO-1715337518874)]

[外链图片转存中…(img-cowlZ1Nl-1715337518875)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android WMS(Window Manager Service)是Android系统中的一个重要组件,它负责管理窗口的创建、移动、调整大小、绘制和显示等操作。在Android系统中,所有的窗口都是由WMS来管理的,包括Activity、DialogToast、PopupWindow等等。WMSAndroid系统中最底层的窗口管理服务,负责协调各个窗口之间的关系,以保证窗口能够正确地显示在屏幕上。 WMS主要包括以下几个部分: 1. 窗口管理器:负责窗口的创建、移动、调整大小、绘制和显示等操作。 2. 窗口策略:负责根据窗口的属性和状态,决定窗口的显示方式和位置。 3. 输入管理器:负责接收用户的输入事件,并将其分派给合适的窗口处理。 4. 动画管理器:负责窗口的动画效果,如打开、关闭、移动等。 5. 窗口回收器:负责回收不需要显示的窗口,以释放资源。 WMS采用了客户端-服务器架构,客户端包括应用程序和系统服务,而WMS则是一个系统服务。当应用程序或系统服务需要创建一个窗口时,它会向WMS发送请求,WMS则根据窗口的属性和状态来决定窗口的显示方式和位置。在窗口创建完成后,WMS会将其添加到窗口列表中,并为其分配一个标识符,以便之后进行管理。 在Android系统中,WMS是一个非常重要的组件,它直接影响着用户的体验和系统的稳定性。因此,了解WMS的工作原理和内部机制,对于Android开发人员来说是非常有必要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值