Window和WindowManager源码解析

46 阅读18分钟

本文源码基于 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 接口:

public interface WindowManager extends ViewManager {

}

ViewManager 接口中只有 3 个方法,分别用来添加、更新和删除 View :

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:

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)) {
                // 开启权限
                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();
        // 调用 addView() 方法添加 Window
        windowManager.addView(button, layoutParams);
    }
}

这里添加的是系统类型的 Window ,需要在 Manifest 文件中添加权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

上述代码将一个 Button 添加到了屏幕上坐标为(100,300)的位置上,其中 WindowManager.LayoutParams 的 flags 和 type 这两个参数比较重要。

Flags 参数可以控制 Window 的显示特性,有下面几个比较常用:

  1. FLAG_NOT_FOCUSABLE,表示该 Window 不需要获取焦点,也不需要接收输入事件,此标记会同时启用 FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的 Window 。
  2. FLAG NOT_TOUCH_MODAL,表示系统会将当前 Window 区域以外的单击事件传递给底层的 Window ,当前 Window 区域以内的单击事件则自己处理。这个标记很重要,一般来说都需要开启此标记,否则其他 Window 将无法收到单击事件。
  3. FLAG_SHOW_WHEN_LOCKED,开启此模式可以让 Window 显示在锁屏的界面上。

Type 参数表示 Window 的类型,有三种,分别是应用 Window 、子 Window 和系统 Window 。应用类 Window 对应着一个 Activity 。子 Window 不能单独存在,它需要附属在特定的父 Window 之中,比如常见的一些 Dialog 就是子 Window 。系统 Window 是需要声明权限才能创建的 Window ,比如 Toast(在 API 26+ 上不需要权限,但有限制:不能获取焦点、不能触摸)和系统状态栏这些都是系统 Window 。

Window 是分层的,每个 Widow 都有对应的 z-ordered ,层级大的会覆盖在层级小的 Window 的上面。在三类 Window 中,应用 Window 的层级范围是 1—99 ,子 Window 的层级范围是 1000—1999 ,系统 Window 的层级范围是 2000—2999 ,这些层级范围对应着 WindowManager.LayoutParams 的 type 参数:

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 类:

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() 方法如下:

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() 方法里面主要做了这些事情:

  1. 检查参数是否合法。
  2. 创建 ViewRootImpl 并将 View 添加到列表中。
  3. 通过 ViewRootImpl 来更新界面并完成 Window 的添加。

ViewRootImpl 的 setView() 方法代码如下:

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        
    public ViewRootImpl(Context context, Display display) {
        this(context, display, WindowManagerGlobal.getWindowSession(),
                false /* useSfChoreographer */);
    }

    public ViewRootImpl(Context context, Display display, IWindowSession session) {
        this(context, display, session, false /* useSfChoreographer */);
    }

    public ViewRootImpl(Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
        mContext = context;
        mWindowSession = session;   
        ...
    }    

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
                        int userId) {
        ...
        res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                mAttachInfo.mDisplayCutout, inputChannel,
                mTempInsets, mTempControls);
        setFrame(mTmpFrame);
        ...
    }
}

Window 的添加是通过 mWindowSession 的 addToDisplayAsUser() 方法来完成的,mWindowSession 的类型是 IWindowSession ,它是一个 Binder 对象,真正的实现类是 Session,也就是 Window 的添加过程是一次 IPC 调用,在 Session 内部会通过 WindowManagerService 来添加 Window :

class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {

    final WindowManagerService mService;
    
    public Session(WindowManagerService service, IWindowSessionCallback callback) {
        mService = service;
        mCallback = callback;
        ...
    }

    @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) {
        // 调用 WindowManagerService 的 addWindow() 方法
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
                outInsetsState, outActiveControls, userId);
    }
}

如此一来,Window 的添加请求就交给了 WMS 去处理了,在 WMS 内部会为每一个应用保留一个单独的 Session。

2.2 Window 的删除过程

Window 的删除过程和添加过程一样,都是先通过 WindowManagerImpl 后,再进一步通过 WindowManagerGlobal 来实现的。下面是 WindowManagerGlobal 的 removeView() 的实现:

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 在 mViews 数组中的索引,然后再调用 removeViewLocked() 方法:

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

先根据索引拿到对应的 ViewRootImpl,参数 immediate 为 false 时表示异步删除, immediate 为 true 时表示同步删除。一般来说不会使用同步删除,以免发生意外。具体的删除操作由 ViewRootImpl 的 die() 方法来完成:

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() 方法内部判断如果是同步删除(立即删除),并且 mlsInTraversal 值为 false,则直接调用 doDie()。mIsInTraversal 在执行 ViewRootlmpl 的 performTraversals() 方法时会被设置为 true,在 performTraversals() 方法执行完时被设置为 false。如果是异步删除,那么就发送一个 MSG_DIE 的消息,ViewRootImpl 中的 Hander 会处理此消息并调用 doDie() 方法。doDie() 方法代码如下:

void doDie() {
    // 检查线程
    checkThread();
    if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
    synchronized (this) {
        // 防止重复执行后续逻辑
        if (mRemoved) {
            return;
        }
        mRemoved = true;
        if (mAdded) {
            dispatchDetachedFromWindow();
        }
        ...
        mAdded = false;
    }
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

先检查线程,然后使用 mRemoved 防止重复执行后续逻辑。在代码第 12 行调用了 dispatchDetachedFromWindow() 方法,然后调用了 WindowManagerGlobal 的 doRemoveView() 方法,代码如下:

void doRemoveView(ViewRootImpl root) {
    boolean allViewsRemoved;
    synchronized (mLock) {
        final int index = mRoots.indexOf(root);
        if (index >= 0) {
            mRoots.remove(index);
            mParams.remove(index);
            final View view = mViews.remove(index);
            mDyingViews.remove(view);
        }
        allViewsRemoved = mRoots.isEmpty();
    }
    if (ThreadedRenderer.sTrimForeground && ThreadedRenderer.isAvailable()) {
        doTrimForeground();
    }

    // If we don't have any views anymore in our process, we no longer need the
    // InsetsAnimationThread to save some resources.
    if (allViewsRemoved) {
        InsetsAnimationThread.release();
    }
}

在 WindowManagerGlobal 中维护了和 Window 操作相关的三个列表,doRemoveView() 方法会从这三个列表中清除 View 对应的元素。

真正删除 View 的逻辑在dispatchDetachedFromWindow() 方法中,dispatchDetachedFromWindow() 方法代码如下:

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() 方法主要做了这些事情:

  1. 调用 View 的 dispatchDetachedFromWindow() 方法,在内部会调用 View 的onDetachedFromWindow() 以及 onDetachedFromWindowInternal()。对于onDetachedFromWindow() 大家一定不陌生,当 View 从 Window 中移除时,这个方法就会被调用,可以在这个方法内部做一些资源回收的工作,比如终止动画、停止线程等。
  2. 垃圾回收相关的工作,比如清除数据和消息、移除回调。
  3. 通过 Session 的 remove() 方法删除 Window——mWindowSession.remove(mWindow),这同样是一个 IPC 过程,最终会调用 WMS 的 removeWindow() 方法。

2.3 Window的更新过程

Window 的更新过程还是要从 WindowManagerGlobal 开始分析:

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 的视图,这个过程最终是由 WMS 的 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() 方法关联上下文环境等参数:

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() 方法代码如下:

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() 方法,该方法里面大致遵循如下几个步骤:

  1. 如果没有 DecorView,那么就创建它。
  2. 将 View 添加到 mContentParent 中。

这些步骤在 Android View的绘制流程 中基本都分析过了,虽然说早在 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 类似,有如下几个步骤:

  1. 创建 Window。Dialog 的构造方法中会新建一个 PhoneWindow。
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);
    }
}
  1. 初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中。这个过程也和 Activity 类似,都是通过 Window 去添加指定的布局文件。
public void setContentView(@LayoutRes int layoutResID) {
    mWindow.setContentView(layoutResID);
}
  1. 将 DecorView 添加到 Window 中并显示。这里会调用 Dialog 的 show() 方法,通过 WindowManager 将 DecorView 添加到 Window 中,如下:
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 就会报错。

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 为系统类型就可以正常弹出对话框,需要添加如下代码:

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() 方法的实现如下:

public void show() {
    ...
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();
    ...
    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);
    }
}

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

public class NotificationManagerService extends SystemService {

    static final int MAX_PACKAGE_NOTIFICATIONS = 25;

    final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();

    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);
                        // 获取 ToastRecord
                        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 为空为止。

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() 方法:

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 线程池中:

public class Toast {

    private static class TN extends ITransientNotification.Stub {

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

这里通过 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;
            }
            ...
        }
    }
};

public void handleShow(IBinder windowToken) {
    ...
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
        mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
                mHorizontalMargin, mVerticalMargin,
                new CallbackBinder(getCallbacks(), mHandler));
    }
}

然后调用了 handleShow() 方法,再调用了 mPresenter.show(),ToastPresenter 的 show() 方法代码如下:

public class ToastPresenter {

    public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
            int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
            @Nullable ITransientNotificationCallback callback) {
        ...
        mWindowManager.addView(mView, mParams);
        ...
    }
}

最终还是调用的 WindowManager 的 addView() 方法添加窗口显示 Toast。

Toast 显示后,NMS 还会通过 scheduleDurationReachedLocked() 方法来发送一个延时消息,具体的延时取决于 Toast 的时长,如下所示:

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 的隐藏。