从源码分析Dialog,PopWindow为啥需要用activity的context创建,而不能用application。

45 篇文章 0 订阅
4 篇文章 0 订阅

关系图

先来一张图展示,activity的结构和内容。可知 activity,phoneWindow,WindowManagerIMPL,viewRootIMPL是一对一的关系,WindowManagerIMPL内部维护一个全局WindowManagerGlobal对象。PhoneWIndow 中对视图的操作,都是通过WindowManagerIMPL代理实现,最终都是ViewRootImpl 和 WMS交互的完成的。

Dialog的创建

  Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
        	//可以看出来,dialog其实也支持主题的,就是个小型的activity,但是必须依附于activity,可以视为activity的子窗口,这个后面再说
            if (themeResId == Resources.ID_NULL) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }
		// 获取wms服务
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		//创建phonewindow
        final Window w = new PhoneWindow(mContext);
        
        ...
		...省略
		
		// 初始化phonewindow,注意后面两个参数为空
		//关联WindowManager与新Window,特别注意第二个参数token为null,也就是说Dialog没有自己的token
        //一个Window属于Dialog的话,那么该Window的mAppToken对象是null,
        //mAppToken对象保存在windowState对象中,对应着这个窗口是否在AMS中有ActivityRecord记录。
		w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

在这里插入图片描述
appToken代表的是activity在AMS服务端的实体记录activityRecord,在ActivityThread内是以ibinder形式存在的,也就是远程通信对象,这个对象很重要,也注定dialog能否展示的关键。
我们来看下这个setWindowManager是怎么处理的:
在这里插入图片描述
也就是一个窗口实例会创建一个WindowManagerIMP的实例,一对一,activity一个对应一个,alertDIalog也对应一个。如果alerDialog的context使用的是activity的话,会获取到activity的WindowManagerIMP的实例,因为activity重写了这个方法。

由于传递的activity的context因此会调用activity重写的getSystemSevice方法,返回的其实是activity的mWIndowManager服务对象。
在这里插入图片描述

//	如果这个context是contextIMP对象的话,那么就是从app全局map中获取的单例,有兴趣自己去查看源码
 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

可以看见Context的WindowManager对每个APP来说是一个全局单例的,而Activity的WindowManager是每个Activity都会新创建一个的(其实你从上面分析的两个实例化WindowManagerImpl的构造函数参数传递就可以看出来,Activity中Window的WindowManager成员在构造实例化时传入给WindowManagerImpl中mParentWindow成员的是当前Window对象,而ContextImpl的static块中单例实例化WindowManagerImpl时传入给WindowManagerImpl中mParentWindow成员的是null值),所以上面模拟苹果浮动小图标使用了Application的WindowManager而不是Activity的,原因就在于这里;使用Activity的WindowManager时当Activity结束时WindowManager就无效了,所以使用Activity的getSysytemService(WINDOW_SERVICE)获取的是Local的WindowManager。同时可以看出来Activity中的WindowManager.LayoutParams的type为TYPE_APPLICATION。

Dialog的show方法

 public void show() { 
 
        if (!mCreated) {
            dispatchOnCreate(null);
        } else { 
        	....
        }
		...
        onStart();
        mDecor = mWindow.getDecorView();
 		...
        WindowManager.LayoutParams l = mWindow.getAttributes();
 		...
 		//这句话是关键
        mWindowManager.addView(mDecor, l);
        ...
        //发送handle消息,通知操作回调
        sendShowMessage();
    }

mWindowManager.addView(mDecor, l);这句话对应文章开头的对象结构图,调用链是:
mWindowManager.addView ===> windowManagerGlobal ===> 创建ViewRootImp ===>
ViewRootImp.setView ===> mWindowSession.addToDisplay 。
mWindowSession是viewRootImp的内部获取的WMS远程对象,用于跨进程调用WMS的方法。
这样就来到了WMS方法内部。

WMS对应窗口的管理



		
	public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        int[] appOp = new int[1];
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }
        
        WindowState parentWindow = null; 
        
		...省略
		
        synchronized (mGlobalLock) {  
        	//这种情况一般不存在,只要正常流程displayContent 都是非空
            if (displayContent == null) {
                Slog.w(TAG_WM, "Attempted to add window to a display that does not exist: "
                        + displayId + ".  Aborting.");
                return WindowManagerGlobal.ADD_INVALID_DISPLAY;
            }
            //displayContent对wms没有访问权限,这个只要是在application中应该都可以使用,不考虑
            if (!displayContent.hasAccess(session.mUid)) {
                Slog.w(TAG_WM, "Attempted to add window to a display for which the application "
                        + "does not have access: " + displayId + ".  Aborting.");
                return WindowManagerGlobal.ADD_INVALID_DISPLAY;
            }
			// 已经缓存的windowmap中是否已经存在该iWindow对象,该对象是viewRootImp在跨进程中的binder对象,
			// dialog多次添加的情况下,不处理,防止重复添加
            if (mWindowMap.containsKey(client.asBinder())) {
                Slog.w(TAG_WM, "Window " + client + " is already added");
                return WindowManagerGlobal.ADD_DUPLICATE_ADD;
            }
			// dialog窗口的类型是 APPLICATION_WINDOW 类型,这里不看,但是popwindow是该类型的
			// 还有就是 surfaceview的窗口类型也是这种,会获取所依附的父窗口,也就是activity
            if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
                parentWindow = windowForClientLocked(null, attrs.token, false);
                if (parentWindow == null) {
                    Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
                }
                if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                        && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                    Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
                            + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
                }
            }

            if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
                Slog.w(TAG_WM, "Attempted to add private presentation window to a non-private display.  Aborting.");
                return WindowManagerGlobal.ADD_PERMISSION_DENIED;
            }
            
      		...省略

上面步骤中,分析了一部分意外情况,还没到dialog的context是非activity的情况。
可以先说下这几个参数是啥:

 Session session, //一个应用会创建一个这个对象,单例保存在viewRootImp中,用于和WMS通信
 IWindow client, //客户端viewRootImp对象的内部类binder实例,传递给WMS,用于WMS远程调用
 LayoutParams attrs, //客户端的windowManagerImp内部的布局对象 

在这里插入图片描述
dialog的layoutParams是TYPE_APPLICATION因此不会执行到 SUB_WINDOW判断里面的内容。

		AppWindowToken atoken = null;
        final boolean hasParent = parentWindow != null;
        // Use existing parent window token for child windows since they go in the same token
        // as there parent window so we can apply the same policy on them.
        WindowToken token = displayContent.getWindowToken(
                hasParent ? parentWindow.mAttrs.token : attrs.token);
        // If this is a child window, we want to apply the same type checking rules as the
        // parent window type.
        final int rootType = hasParent ? parentWindow.mAttrs.type : type;

        boolean addToastWindowRequiresToken = false;

        if (token == null) {
            if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
                Slog.w(TAG_WM, "Attempted to add application window with unknown token "
                      + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }

再往下看,getWindowToken是根据我们传递进来的attrs.token也就是客户端的IWindow对象,从map集合中获取windowToken对象,很显然,如果我们不是传递的activity的token,那么map集合中不可能存在iwindow对象,因为我们都没有保存过怎么可能存在这个对象。这样走走到了token==null的判断里面去了,我们上面说过alertDialog的窗口类型就是APPLICATION_WINDOW类型的,因此下面就会返回WindowManagerGlobal.ADD_BAD_APP_TOKEN;这个错误给我们的客户端。

viewRootIMP对于返回结果的处理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于我的手机没展示错误就闪退了,只能看日志报错,所有上面日志的报错就对应着,viewrootIMP所抛出的异常错误,结果很明显了,就是application之所以不能创建dialog对象,是因为WMS服务端查询不到对应的windowToken对象,所有会报错误给客户端。

popWindow的创建过程

popWindow创建过程和AlertDialog差不多不赘述,来看下如何展示的:
在这里插入图片描述
可以看到需要外部传入一个view,然后获取这个view的windowToken,而创建的窗口类型是:

 /**
   * Window type: a panel on top of an application window.  These windows
      * appear on top of their attached window.
*/
  public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
  private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;

上面看到的就是popupWindow的窗口类型,就是子窗口类型,如果我们使用的是或者不是activity的Token的话,我们按照这个思路去WMS寻找下答案。

		
       if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
           parentWindow = windowForClientLocked(null, attrs.token, false);
           if (parentWindow == null) {
               Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
                     + attrs.token + ".  Aborting.");
               return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
           }
           if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                   && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
               Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
                       + attrs.token + ".  Aborting.");
               return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
           }
       }

那么进入到这个if判断里面去,parentWindow是根据attrs.token来获取的,也就是这个Token对象对应的WindowState,WindowState对象是activity第一次添加窗口的时候创建的,成员变量是mAppToken,保存的是AMS中传递过来ActivityReord对象来进行创建的,因此如果我们的Token不是对应着AMS中的activity的话,那么这个parentWindow也就是个null,接着就会报异常给客户端,viewRootIMP中回对异常进行处理,看下是怎么处理的:
在这里插入图片描述
一样的和AlertDialog的异常是一样的。

偷老罗的一张图展示下,三个进程之间的关系,在我们的AMS的通知我们的ActivityThread进行activity的启动的时候,在AMS中相应把ActivityRecord对象传递给WMS中,在WMS中创建AppWindowToken对象,在Activity第一次添加窗口到WMS中的时候,会将viewRootIMP的iWindow对象,appWindowToken,作为成员变量,创建出windowState对象,这样我们接着在activity中启动AlertDialog的话,添加dialog的window到WMS中,判断这个token是否已经存在,显然如果是activity的Token肯定可以找得到,如果是application的Token那明显是空的,所以会报错。

在这里插入图片描述
参考:
https://blog.csdn.net/Luoshengyang/article/details/8275938
https://blog.csdn.net/yanbober/article/details/46361191

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用vue-draggable-resizable插件来实现element dialog顶部拖拽的效果,并且限制其不能拖离页面。此插件支持拖拽、缩放、限制边界等功能,使用非常方便。 首先,安装vue-draggable-resizable插件: ``` npm install vue-draggable-resizable ``` 然后,在需要使用拖拽效果的组件中引入该插件并注册: ```javascript <template> <el-dialog :visible.sync="dialogVisible"> <div class="dialog-header" v-draggable> <span>Dialog Title</span> </div> <div class="dialog-body"> Dialog Content </div> </el-dialog> </template> <script> import VueDraggableResizable from 'vue-draggable-resizable' export default { components: { VueDraggableResizable }, data() { return { dialogVisible: false } } } </script> ``` 在上述代码中,我们为dialog的顶部header添加了一个类名为`dialog-header`的div,并且在该div上使用了`v-draggable`指令,使其具有了拖拽的能力。 需要注意的是,为了限制dialog不能拖离页面,我们需要在该指令的定义中添加一些限制逻辑。具体来说,我们需要在`v-draggable`指令中传入一个配置对象,如下: ```javascript <div class="dialog-header" v-draggable="{ containment: '.el-dialog__wrapper', axis: 'y' }"> ``` 上述代码中,`containment`属性指定了限制拖拽区域的父级元素选择器,这里选择了`.el-dialog__wrapper`,表示dialog不能拖离该元素所在的区域。 `axis`属性指定了拖拽的方向,这里选择了`y`,表示只能在垂直方向上拖拽。 这样,就可以实现element dialog顶部拖拽,并且限制其不能拖离页面的效果了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值