本文源码基于Android 11.0
一、Window和WindowManager
Window是一个抽象类,其唯一具体实现是PhoneWindow。Android中的所有视图都是通过Window来呈现的,不管是Actvity、Dialog还是Toast,它们的视图实际都是附加在Window上的,因此Window是View的直接管理者。当我们调用Activity的setContentView()方法时,最终会调用Window的setContentView(),当我们调用Activity的findViewById()时,其实最终调用的是Window的findViewById()。
WindowManager是一个接口,从名称就可以看出来,它是用来管理窗口的,它继承自ViewManager接口:
java
public interface WindowManager extends ViewManager {
}
ViewManager接口中只有3个方法,分别用来添加、更新和删除View:
java
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
WindowManager继承自ViewManager,也就拥有了这3个功能。
使用WindowManager的addView()方法即可添加一个Window:
java
public class MainActivity extends AppCompatActivity {
private static final int OVERLAY_PERMISSION_REQ_CODE = 12;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void openWindow(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
if (!Settings.canDrawOverlays(this)) {
//打开设置页
Toast.makeText(this, "can not DrawOverlays", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + MainActivity.this.getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
}else {
//已经打开了权限
addWindow();
}
}else {
//6.0以下直接在Manifest中申请权限就行了。
addWindow();
}
}
private void addWindow(){
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.x = 100;
layoutParams.y = 300;
Button button = new Button(this);
button.setText("button");
WindowManager windowManager = this.getWindowManager();
//添加Window
windowManager.addView(button, layoutParams);
}
}
这里添加的Window是系统类型的Window,需要在Manifest文件中添加权限:
xml
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
上述代码将一个Button添加到了屏幕上坐标为(100,300)的位置上,其中WindowManager.LayoutParams的flags和type这两个参数比较重要。
Flags参数可以控制Window的显示特性,有下面几个比较常用:
- FLAG_NOT_FOCUSABLE,表示Window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window。
- FLAG NOT_TOUCH_MODAL,表示系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。这个标记很重要,一般来说都需要开启此标记,否则其他Window将无法收到单击事件。
- FLAG_SHOW_WHEN_LOCKED,开启此模式可以让Window显示在锁屏的界面上。
Type参数 表示Window的类型,Window有三种类型,分别是应用Window、子Window和系统Window。应用类Window对应着一个Activity。子Window不能单独存在,它需要附属在特定的父Window之中,比如常见的一些Dialog就是一个子Window。系统Window是需要声明权限才能创建的Window,比如Toast和系统状态栏这些都是系统Window。
Window是分层的,每个Widow都有对应的z-ordered,层级大的会覆盖在层级小的Window的上面。在三类Window中,应用Window的层级范围是1---99,子Window的层级范围是1000---1999,系统Window的层级范围是2000---2999,这些层级范围对应着WindowManager.LayoutParams的type参数:
java
public interface WindowManager extends ViewManager {
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
@WindowType
public int type;
public static final int FIRST_APPLICATION_WINDOW = 1;
public static final int TYPE_BASE_APPLICATION = 1;
public static final int TYPE_APPLICATION = 2;
public static final int TYPE_APPLICATION_STARTING = 3;
...
public static final int LAST_APPLICATION_WINDOW = 99;
public static final int FIRST_SUB_WINDOW = 1000;
...
public static final int LAST_SUB_WINDOW = 1999;
public static final int FIRST_SYSTEM_WINDOW = 2000;
...
public static final int LAST_SYSTEM_WINDOW = 2999;
}
}
如果想要Window位于所有Window的最顶层,那么采用较大的层级即可。很显然系统Window的层级是最大的,而且系统层级有很多值,一般我们可以选用TYPE_SYSTEM_OVERLAY或者TYPE_SYSTEM_ERROR。
二、Window的内部机制
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,因此Window并不是实际存在的,它是以View的形式存在的。这点从WindowManager的定义也可以看出,它提供的三个接口方法addView()、updateViewLayout()以及removeView()都是针对View的,这说明View才是Window存在的实体。在实际使用中无法直接访问Window,对Window的访问必须通过WindowManager。为了分析Window的内部机制,这里从Window的添加、删除以及更新说起。
2.1 Window的添加
Window的添加过程需要通过WindowManager的addView()方法来实现,WindowManager是一个接口,它的真正实现是WindowManagerImpl类:
java
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
}
可以发现,WindowManagerImpl并没有直接实现Window的三大操作,而是全部交给了WindowManagerGlobal来处理,WindowManagerGlobal以工厂的形式向外提供自己的实例,WindowManagerImpl这种工作模式是典型的桥接模式。
WindowManagerGlobal的addView()方法如下:
java
public final class WindowManagerGlobal {
//存储的是所有Window所对应的View
private final ArrayList<View> mViews = new ArrayList<View>();
//存储的是所有Window所对应的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//存储的是所有Window所对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
//存储的是那些正在被删除的View对象,或者说是那些已经调用remoView()方法但是删除操作还未完成的Window对象
private final ArraySet<View> mDyingViews = new ArraySet<View>();
private static WindowManagerGlobal sDefaultWindowManager;
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
//检查参数的合法性
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
...
//如果是子Window还需要调整布局参数
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
ViewRootImpl root;
...
//创建ViewRootImpl实例
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 {
//更新界面并完成Window的添加
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
addView()方法里面主要做了这些事情:
- 检查参数是否合法。
- 创建ViewRootImpl并将View添加到列表中。
- 通过ViewRootImpl来更新界面并完成Window的添加。
Window的添加是通过mWindowSession的addToDisplayAsUser()方法来完成的,mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是Window的添加过程是一次IPC调用,在Session内部会通过WindowManagerService来添加Window:
java
class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
final WindowManagerService mService;
@Override
public int addToDisplayAsUser(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, int userId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
outInsetsState, outActiveControls, userId);
}
}
如此一来,Window的添加请求就交给了WindowManagerService去处理了,在WindowManagerService内部会为每一个应用保留一个单独的Session。
2.2 Window的删除过程
Window的删除过程和添加过程一样,都是先通过WindowManagerImpl后,再进一步通过WindowManagerGlobal来实现的。下面是 WindowManagerGlobal的removeView()的实现:
java
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
里面首先通过findViewLocked()来查找待删除的View的索引,就是去上面的mView中查找要删除的View,然后再调用removeViewLocked()方法:
java
private void removeViewLocked(int index, boolean immediate) {
//找到对应的ViewRootImpl
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (root != null) {
root.getImeFocusController().onWindowDismissed();
}
//调用ViewRootImpl的die()方法来删除
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
参数immediate为false时表示异步删除,immediate为true时表示同步删除。一般来说不会使用同步删除,以免发生意外。具体的删除操作由ViewRootImpl的die()方法来完成:
java
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
die()方法内部判断如果是同步删除(立即删除),直接调用doDie(),内部调用dispatchDetachedFromWindow()方法,真正删除View的逻辑就在dispatchDetachedFromWindow()方法中。如果是异步删除,那么就发送一个MSG_DIE的消息,ViewRootImpl中的Hander会处理此消息并调用doDie()方法。
dispatchDetachedFromWindow()方法代码如下:
java
void dispatchDetachedFromWindow() {
mInsetsController.onWindowFocusLost();
mFirstInputStage.onDetachedFromWindow();
if (mView != null && mView.mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
//回调View的dispatchDetachedFromWindow()方法
mView.dispatchDetachedFromWindow();
}
//移除各种回调
mAccessibilityInteractionConnectionManager.ensureNoConnection();
mAccessibilityManager.removeAccessibilityStateChangeListener(
mAccessibilityInteractionConnectionManager);
mAccessibilityManager.removeHighTextContrastStateChangeListener(
mHighContrastTextManager);
removeSendWindowContentChangedCallback();
destroyHardwareRenderer();
setAccessibilityFocus(null, null);
mInsetsController.cancelExistingAnimations();
//清除数据
mView.assignParent(null);
mView = null;
mAttachInfo.mRootView = null;
destroySurface();
if (mInputQueueCallback != null && mInputQueue != null) {
mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
mInputQueue.dispose();
mInputQueueCallback = null;
mInputQueue = null;
}
try {
//删除Window
mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
// Dispose receiver would dispose client InputChannel, too. That could send out a socket
// broken event, so we need to unregister the server InputChannel when removing window to
// prevent server side receive the event and prompt error.
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
mInputEventReceiver = null;
}
mDisplayManager.unregisterDisplayListener(mDisplayListener);
unscheduleTraversals();
}
dispatchDetachedFromWindow()方法主要做了这些事情:
- 调用View的dispatchDetachedFromWindow()方法,在内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal()。对于onDetachedFromWindow()大家一定不陌生,当View从Window中移除时,这个方法就会被调用,可以在这个方法内部做一些资源回收的工作,比如终止动画、停止线程等。
- 垃圾回收相关的工作,比如清除数据和消息、移除回调。
- 通过Session的remove()方法删除Window: mWindowSession.remove(mWindow),这同样是一个IPC过程,最终会调用WindowManagerService的removeWindow()方法。
2.3 Window的更新过程
Window的更新过程还是要从WindowManagerGlobal开始分析:
java
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
updateViewLayout()方法做的事情就比较简单了,首先它需要更新View的LayoutParams,接着再更新 ViewRootImpl中的LayoutParams,这一步是通过ViewRootImpl的setLayoutParams()方法来实现的,里面会调用scheduleTraversals()方法来对View重新布局,包括测量、布局、重绘这三个过程。除了View本身的重绘以外, ViewRootImpl还会通过WindowSession来更新Window的视图,这个过程最终是由WindowManagerService的relayoutWindow()来具体实现的,它同样是一个IPC过程。
三、Window的创建过程
通过上面的分析可以看出,View是Android中的视图的呈现方式,但是View不能单独存在,它必须附着在Window这个抽象的概念上面,因此有视图的地方就有Window。哪些地方有视图呢?Android中可以提供视图的地方有Activity、Dialog、Toast,除此之外,还有一些依托Window而实现的视图,比如PopUpWindow、菜单,它们也是视图,有视图的地方就有Window,因此Activity、Dialog、Toast 等视图都对应着一个Window。
3.1 Activity的Window的创建过程
在Activity的启动流程中,会调用ActivityThread的performLaunchActivity()来完成整个启动过程,里面会通过类加载器创建Activity的实例对象,然后调用其attach()方法关联上下文环境等参数:
java
public final class ActivityThread extends ClientTransactionHandler {
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//创建Activity实例
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
//attach()方法为Activity关联上下文环境
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);
return activity;
}
}
ActivityThread的attach()方法代码如下:
java
public class Activity extends ContextThemeWrapper
final void attach(...) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
...
}
}
在attach()方法中,系统会创建Activity所属的Window对象并为其设置回调接口,其中Window的具体实现是PhoneWindow。我们使用setContentView()方法,实际调用的是PhoneWindow的setContentView()方法,该方法里面大致遵循如下几个步骤:
- 如果没有DecorView,那么就创建它。DecorView是Activity的顶级View,一般来说它内部包含标题栏和内容栏,但是这个会随着主题的变换而发生改变。其中内容栏是一定要存在的,内容栏在布局中对应的id为android.R.id.content。DecorView的创建过程由installDecor()来完成,在其内部会通过generateDecor()方法来直接创建DecorView,这个时候DecorView只是一个空白的FrameLayout:
java
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}
为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout()方法来加载具体的布局文件到DecorView中,具体的布局文件和系统版本以及主题有关,这个过程如下所示:
java
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource;
int features = getLocalFeatures();
//根据主题确定具体的布局:layoutResource
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;
}
...
} else {
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//把layoutResource添加到mDecor
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
这里ID_ANDROID_CONTENT对应的ViewGroup就是内容栏,最终会返回给PhoneWindow的mContentParent。在DecorView的onResourcesLoaded()方法中把layoutResource对应的布局添加进mDecor:
java
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
final View root = inflater.inflate(layoutResource, null);
...
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
...
}
- 将View添加到mContentParent中。这个过程就比较简单了,由于在步骤1中已经创建并初始化了DecorView,因此这一步直接将Activity的视图添加到DecorView的mContentParent中即可: mLayoutInflater.inflate(layoutResID,mContentParent)。到此为止,Activity的布局文件已经添加到DecorView里面了,由此可以理解Activity 的setContentView()这个方法的来历了。你是否曾经怀疑过:为什么不叫setView()呢?它明明是给Activity设置视图的啊!从这里来看,它的确不适合叫setView(),因为Activity的布局文件只是被添加到DecorView的mContentParent中,因此叫setContentView()更加准确。
- 回调Activity的onContentChanged方法通知Activity视图已经发生改变。这个过程就更简单了,由于Activity实现了Window的Callback接口,这里表示Activity的布局文件已经被添加到DecorView的mContentParent中了,于是需要通知Activity,使其可以做相应的处理。Activity的onContentChanged 方法是个空实现,我们可以在子Activity中处理这个回调,代码如下:
java
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
经过上面的三个步骤,DecorView已经被创建并初始化完毕,Activity的布局文件也已经成功添加到了 DecorView的mContentParent中,但这时DecorView还没有被WindowManager正式添加到Window中。这里需要正确理解Window的概念,Window更多表示的是一种抽象的功能集合,虽然说早在Activity的attach()方法中 Window就已经被创建了,但是这个时候由于DecorView并没有被WindowManager识别,所以这个时候的Window无法提供具体功能,因为它还无法接收外界的输入信息。在ActivityThread的handleResumeActivity()方法中,首先会调用Activity的onResume方法,接着会调用wm.addView(decor, l),最后调用r.activity.makeVisible(),DecorView真正地完成了添加和显示这两个过程,到这里Activity的视图才能被用户看到。
3.2 Dialog的Window的创建过程
Dialog的Window的创建过程与Activity类似,有如下几个步骤:
- 创建Window。Dialog的构造方法中会新建一个PhoneWindow。
java
public class Dialog{
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
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);
}
}
- 初始化DecorView并将Dialog的视图添加到DecorView中。这个过程也和Activity类似,都是通过Window去添加指定的布局文件。
java
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
- 将DecorView添加到Window中并显示。这里会调用Dialog的show()方法,通过WindowManager将DecorView添加到Window中,如下:
java
public void show() {
...
mDecor = mWindow.getDecorView();
...
mWindowManager.addView(mDecor, l);
...
mShowing = true;
...
}
从上面3个步骤可以发现,Dialog的Window创建与Activity几乎没有什么区别。当Dialog被关闭时,调用的是mWindowManager.removeViewImmediate(mDecor)来移除DecorView。
普通Dialog还有一个特殊之处,就是必须采用Activity的Context,如果采用Application的Context就会报错。
java
Dialog dialog=new Dialog(this.getApplicationContext());
TextView textView = new TextView(this);
textView.setText("this is toast!");
dialog.setContentView(textView);
dialog.show();
报错信息为:Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid
上面的错误信息很明确,是没有应用token导致的,而应用token一般只有Activity拥有,所以这里需要用Activity作为Context来显示对话框即可。另外,系统Window比较特殊,它可以不需要token,因此在上面的例子中,只需要指定对话框的Window为系统类型就可以正常弹出对话框,添加如下代码:
java
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
当然还需要添加相应的权限。
3.3 Toast的Window的创建过程
Toast与Dialog不同,它的工作过程稍显复杂。首先Toast也是基于Window来实现的,但是由于Toast具有定时取消这一功能,所以系统采用了Handler。在Toast的内部有两类IPC过程,第一类是Toast访问NotificationManagerService(NMS),第二类是NMS回调Toast里的TN接口。
Toast属于系统Window,它内部的视图由两种方式指定,一种是系统默认的样式,另一种是通过setView()来指定一个自定义View,不管如何,它们都对应Toast的一个View类型的内部成员mNextView。Toast提供了show()和cancel()分别用于显示和隐藏Toast,它们的内部是一个IPC过程,show()方法和cancel()方法的实现如下:
java
public void show() {
...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
// It's a custom toast
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
// It's a text toast
ITransientNotificationCallback callback =
new CallbackBinder(mCallbacks, mHandler);
service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
}
} else {
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
}
} catch (RemoteException e) {
// Empty
}
}
public void cancel() {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)
&& mNextView == null) {
try {
getService().cancelToast(mContext.getOpPackageName(), mToken);
} catch (RemoteException e) {
// Empty
}
} else {
mTN.cancel();
}
}
从上面的代码可以看到,显示和隐藏Toast都需要通过NMS来实现,由于NMS运行在系统的进程中,所以只能通过远程调用的方式来显示和隐藏Toast。需要注意的是TN这个类,它是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示或隐藏请求时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。这里的当前线程是指发送Toast请求所在的线程。注意,由于这里使用了Handler,所以这意味着Toast无法在没有Looper的线程中弹出,这是因为Handler需要使用Looper才能完成切换线程的功能。
先来看看Toast的显示过程,service.enqueueToast()调用了NotificationManagerService的mService的 enqueueToast():
java
public class NotificationManagerService extends SystemService {
static final int MAX_PACKAGE_NOTIFICATIONS = 25;
final IBinder mService = new INotificationManager.Stub() {
@Override
public void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration,
int displayId, @Nullable ITransientNotificationCallback callback) {
enqueueToast(pkg, token, text, null, duration, displayId, callback);
}
@Override
public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
int duration, int displayId) {
enqueueToast(pkg, token, null, callback, duration, displayId, null);
}
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
@Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
...
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, token);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package can enqueue.
// Prevents DOS attacks and deals with leaks.
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;
}
}
}
Binder windowToken = new Binder();
mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId);
record = getToastRecord(callingUid, callingPid, pkg, token, text, callback,
duration, windowToken, displayId, textCallback);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveForToastIfNeededLocked(callingPid);
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated, show it.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if (index == 0) {
//显示当前的Toast
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
}
}
enqueueToast()首先判断mToastQueue中有没有这个ToastRecord,如果有的话,更新Toast时长。如果没有,将Toast请求封装为ToastRecord对象并将其添加到mToastQueue的队列中,mToastQueue其实是一个ArrayList。对于非系统应用来说,mToastQueue中最多能同时存在25个ToastRecord,这样做是为了防止DOS(Denial of Service:拒绝服务)。如果不这么做,试想一下,如果我们通过大量的循环去连续弹出Toast,这将会导致其他应用没有机会弹出Toast,那么对于其他应用的Toast请求,系统的行为就是拒绝服务,这就是拒绝服务攻击的含义,这种手段常用于网络攻击中。
正常情况下,一个应用不可能达到上为空为止。被添加到mToastQueue中后,NMS就会通过showNextToastLocked()方法来显示当前的Toast。该方法里面先获取mToastQueue中的第1个ToastRecord,然后进入While循环------显示Toast,把当前ToastRecord从mToastQueue中移除,再重新获取第1个ToastRecord,直到mToastQueue为空为止。
java
void showNextToastLocked() {
//获取mToastQueue中的第1个ToastRecord
ToastRecord record = mToastQueue.get(0);
//While循环
while (record != null) {
//显示Toast
if (record.show()) {
scheduleDurationReachedLocked(record);
return;
}
int index = mToastQueue.indexOf(record);
if (index >= 0) {
//从mToastQueue中移除当前ToastRecord
mToastQueue.remove(index);
}
//重新获取mToastQueue中的第1个ToastRecord
record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
}
}
首先来看看record.show(),ToastRecord是一个抽象类,实现类是CustomToastRecord,实际调用的是CustomToastRecord的show()方法:
java
public class CustomToastRecord extends ToastRecord {
public CustomToastRecord(NotificationManagerService notificationManager, int uid, int pid,
String packageName, IBinder token, ITransientNotification callback, int duration,
Binder windowToken, int displayId) {
super(notificationManager, uid, pid, packageName, token, duration, windowToken, displayId);
this.callback = checkNotNull(callback);
}
@Override
public boolean show() {
...
callback.show(windowToken);
return true;
...
}
}
里面是通过callback来完成的,这个callback实际是TN对象的远程Binder,通过callback来访问TN中的方法需要跨进程来完成,最终被调用的TN的方法会运行在发起Toast请求的应用的Binder线程池中:
java
public class Toast {
private static class TN extends ITransientNotification.Stub {
public void show(IBinder windowToken) {
...
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
}
}
这里通过Handler发消息切换到主线程:
java
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
...
}
}
};
然后调用了handleShow()方法,再调用了mPresenter.show(),最终还是调用的WindowManager的addView()方法添加窗口显示Toast。
Toast显示后,NMS还会通过scheduleDurationReachedLocked()方法来发送一个延时消息,具体的延时取决于Toast的时长,如下所示:
java
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
int delay = r.getDuration() == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
AccessibilityManager.FLAG_CONTENT_TEXT);
mHandler.sendMessageDelayed(m, delay);
}
在上面的代码中,LONG_DELAY是3.5s,而SHORT_DELAY是2s。延迟相应的时间后,NMS会通过cancelToastLocked()方法来隐藏Toast并将其从mToastQueue中移除,这个时候如果mToastQueue中还有其他Toast,那么NMS就继续显示其他Toast。Toast的隐藏也是通过ToastRecord的callback来完成的,这同样也是一次IPC过程,它的工作方式和Toast的显示过程是类似的。最终调用了WindowManager的removeView()方法来完成Toast的隐藏。