深入理解WMS(二):Dialog与Toast源码解析,Android进程保活黑科技实现原理解密及方法

69 阅读7分钟

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

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

/* ignore */

}

......

上面的handleShow代码段,我们能清楚的看到,mWM将Toast添加了进去。handleHide的源码如下:

public void handleHide() {

最后

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

以上进阶BATJ大厂学习资料可以免费分享给大家,需要完整版的朋友,【点这里可以看到全部内容】。