美文网首页
Window和WindowManager源码解析

Window和WindowManager源码解析

作者: 头秃到底 | 来源:发表于2024-03-17 18:02 被阅读0次

    本文源码基于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)) {
                    //打开设置页
                    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文件中添加权限:

    <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参数:

    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的添加。

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

    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()的实现:

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

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

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

    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开始分析:

    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()方法关联上下文环境等参数:

    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,那么就创建它。DecorView是Activity的顶级View,一般来说它内部包含标题栏和内容栏,但是这个会随着主题的变换而发生改变。其中内容栏是一定要存在的,内容栏在布局中对应的id为android.R.id.content。DecorView的创建过程由installDecor()来完成,在其内部会通过generateDecor()方法来直接创建DecorView,这个时候DecorView只是一个空白的FrameLayout:
    protected DecorView generateDecor(int featureId) {
        ...
        return new DecorView(context, featureId, this, getAttributes());
    }
    
    

    为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout()方法来加载具体的布局文件到DecorView中,具体的布局文件和系统版本以及主题有关,这个过程如下所示:

    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:

    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中处理这个回调,代码如下:
    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。
    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();
    
        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():

    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为空为止。

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

    然后调用了handleShow()方法,再调用了mPresenter.show(),最终还是调用的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的隐藏。

    相关文章

      网友评论

          本文标题:Window和WindowManager源码解析

          本文链接:https://www.haomeiwen.com/subject/cqxazdtx.html