Window和WindowManager源码解析

本文源码基于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的显示特性,有下面几个比较常用:

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

  1. 检查参数是否合法。
  2. 创建ViewRootImpl并将View添加到列表中。
  3. 通过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()方法主要做了这些事情:

  1. 调用View的dispatchDetachedFromWindow()方法,在内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal()。对于onDetachedFromWindow()大家一定不陌生,当View从Window中移除时,这个方法就会被调用,可以在这个方法内部做一些资源回收的工作,比如终止动画、停止线程等。
  2. 垃圾回收相关的工作,比如清除数据和消息、移除回调。
  3. 通过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()方法,该方法里面大致遵循如下几个步骤:

  1. 如果没有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));
    ...
}
  1. 将View添加到mContentParent中。这个过程就比较简单了,由于在步骤1中已经创建并初始化了DecorView,因此这一步直接将Activity的视图添加到DecorView的mContentParent中即可: mLayoutInflater.inflate(layoutResID,mContentParent)。到此为止,Activity的布局文件已经添加到DecorView里面了,由此可以理解Activity 的setContentView()这个方法的来历了。你是否曾经怀疑过:为什么不叫setView()呢?它明明是给Activity设置视图的啊!从这里来看,它的确不适合叫setView(),因为Activity的布局文件只是被添加到DecorView的mContentParent中,因此叫setContentView()更加准确。
  2. 回调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类似,有如下几个步骤:

  1. 创建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);
    }
}    
  1. 初始化DecorView并将Dialog的视图添加到DecorView中。这个过程也和Activity类似,都是通过Window去添加指定的布局文件。
java 复制代码
public void setContentView(@LayoutRes int layoutResID) {
    mWindow.setContentView(layoutResID);
}
  1. 将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的隐藏。

相关推荐
工业互联网专业6 小时前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
IT研究室5 天前
大数据毕业设计选题推荐-电影数据分析系统-数据可视化-Hive-Hadoop-Spark
大数据·hive·hadoop·spark·毕业设计·源码·课程设计
IT毕设梦工厂5 天前
大数据毕业设计选题推荐-NBA球员数据分析系统-Python数据可视化-Hive-Hadoop-Spark
大数据·hive·hadoop·spark·毕业设计·源码·课程设计
IT研究室5 天前
大数据毕业设计选题推荐-民族服饰数据分析系统-Python数据可视化-Hive-Hadoop-Spark
大数据·hive·hadoop·spark·毕业设计·源码·课程设计
一 乐5 天前
高校体育场小程序|高校体育场管理系统系统|体育场管理系统小程序设计与实现(源码+数据库+文档)
数据库·小程序·vue·源码·springboot·体育馆小程序
工业互联网专业6 天前
毕业设计选题:基于springboot+vue+uniapp的在线办公小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
一 乐7 天前
畅阅读小程序|畅阅读系统|基于java的畅阅读系统小程序设计与实现(源码+数据库+文档)
java·小程序·vue·源码·springboot·阅读小程序
一 乐7 天前
助农小程序|助农扶贫系统|基于java的助农扶贫系统小程序设计与实现(源码+数据库+文档)
java·数据库·小程序·vue·源码·助农
一 乐7 天前
订餐点餐|订餐系统基于java的订餐点餐系统小程序设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·源码
Iareges8 天前
PyTorch源码系列(一)——Optimizer源码详解
人工智能·pytorch·python·源码·优化算法·sgd·optimizer