WindowManager$BadTokenException-解决方案

简介

上一篇分析了WindowManager$BadTokenException发生的原因,带大家一起通过分析WindowManager源码,更加深入的了解了WindowManager添加window的过程,以及在使用WindowManager添加自己的window或者View的时候,怎么去避免发生异常,接下来,继续深入分析WindowManager源码,带大家一起寻找,解决平时使用WindowManager出现的各种异常的办法。

\color{blue}{建议:}本文章讲解,建立在上一篇文章的基础之上,所以阅读本文章前,请先阅读上一篇文章,地址如下:
WindowManager$BadTokenException(WindowManager源码分析

源码版本

在没有特别说明的情况下,源码版本如下:

  • sdk:android-28
  • Android系统源码:Android8.0

window类型

Window有三种类型,分别是应用Window,子Window和系统Window。

  • 应用类Window对应着一个Activity。

  • 子Window不能单独存在,它需要附属在特定的父Window中,比如Dialog就是一个子Window。

  • 系统Window是需要声明权限才能创建的Window,比如Toast和系统状态栏这些都是系统Window。

    Window是分层的,每个Window都有对应的z-ordered,层级大的会覆盖    在层级小的Window上。在三类    
    Window中,应用Window的层级范围是1~99,子Window的层级范围是1000~1999,系统Window的层级范 
    围是2000~2999。很显然系统Window的层级是最大的,而且系统层级有很多值,一般我们可以选用 
    TYPE_SYSTEM_ERROR或者TYPE_SYSTEM_OVERLAY,另外重要的是要记得在清单文件中声明权限。
    

系统Window

由于源码比较多,只讲解关键或者不容易判断的代码,其它可以自行查看。

  • 函数 :addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow)
    源码文件:WindowManagerGlobal.java
  1. root = new ViewRootImpl(view.getContext(), display);
    创建ViewRootImpl,看一下里面创建的几个关键的对象
    (1) mWindow = new W(this);
    (2) mWindowSession = WindowManagerGlobal.getWindowSession();
  • 函数:setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
    源码文件:ViewRootImpl.java
  1. 参数变化:
    (1) attrs
    mWindowAttributes.copyFrom(attrs);
    if (mWindowAttributes.packageName == null) {
        mWindowAttributes.packageName = mBasePackageName;
    }
    attrs = mWindowAttributes;
    
  • 函数:addWindow(Session session, IWindow client, int seq,WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets,InputChannel outInputChannel)
  1. 参数
    (1)session: mWindowSession
    (2)client:mWindow
    (3)attrs:mWindowAttributes包含了传入时WindowManager.LayoutParams参数的所有属性

  2. 权限检测: int res = mPolicy.checkAddPermission(attrs, appOp)
    函数:checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp)
    源码路径:frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
    (1)如果不在window类型里,返回无效类型 —— ADD_INVALID_TYPE

     if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
              || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
              || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
          return WindowManagerGlobal.ADD_INVALID_TYPE;
      }
    

    (2)不是系统窗体类型(即应用window和子window)和高于最后一个系统window类型的,直接返回 —— ADD_OKAY,不再进行权限检测。

     if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
          // Window manager will make sure these are okay.
          return ADD_OKAY;
      }
    

    (3) 如果不是系统弹窗window,除了一下类型,者需要INTERNAL_SYSTEM_WINDOW权限(系统app才能申请的权限)

      //WindowManager.java
      public static boolean isSystemAlertWindowType(int type) {
          switch (type) {
              case TYPE_PHONE:
              case TYPE_PRIORITY_PHONE:
              case TYPE_SYSTEM_ALERT:
              case TYPE_SYSTEM_ERROR:
              case TYPE_SYSTEM_OVERLAY:
              case TYPE_APPLICATION_OVERLAY:
                  return true;
          }
          return false;
      }
    
      if (!isSystemAlertWindowType(type)) {
          switch (type) {
              case TYPE_TOAST:
                  outAppOp[0] = OP_TOAST_WINDOW;
                  return ADD_OKAY;
              case TYPE_DREAM:
              case TYPE_INPUT_METHOD:
              case TYPE_WALLPAPER:
              case TYPE_PRESENTATION:
              case TYPE_PRIVATE_PRESENTATION:
              case TYPE_VOICE_INTERACTION:
              case TYPE_ACCESSIBILITY_OVERLAY:
              case TYPE_QS_DIALOG:
                  // The window manager will check these.
                  return ADD_OKAY;
          }
          return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                  == PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
      }
    
      if (appInfo == null || (type != TYPE_APPLICATION_OVERLAY && appInfo.targetSdkVersion >= O)) {
          return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                  == PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
      }
    

    (4) 检测app是否申明或者在设备里面以及打开了权限

      final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid, attrs.packageName);
      switch (mode) {
          case AppOpsManager.MODE_ALLOWED:
          case AppOpsManager.MODE_IGNORED:
              return ADD_OKAY;
          case AppOpsManager.MODE_ERRORED:
              if (appInfo.targetSdkVersion < M) {
                  return ADD_OKAY;
              }
              return ADD_PERMISSION_DENIED;
          default:
            //默认需要SYSTEM_ALERT_WINDOW权限
              return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)
                      == PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
        ```
    
  3. 类型检测
    (1)callingUid,type
    \color{blue}{注意:}现在是在系统进程里面,window添加过程其实是跨进程的,这句话意思是获取调用进程的uid,即我们app所在的进程uid
    final int callingUid = Binder.getCallingUid();
    //这个是我们传入的window类型
    final int type = attrs.type;
    (2) mWindowMap

        final WindowState win = new WindowState(this, session, client, token, parentWindow,
                 appOp[0], seq, attrs, viewVisibility, session.mUid,
                 session.mCanAddInternalSystemWindow);
        //client:mWindow
        mWindowMap.put(client.asBinder(), win);
    
  4. 异常:
    (1) Unable to add window -- window mWindow has already been added

         if (mWindowMap.containsKey(client.asBinder())) {
             return WindowManagerGlobal.ADD_DUPLICATE_ADD;
         }
    

意思是同一个window添加多次,但是通过addview方法,都会重新创建ViewRootImpl对象,然后重新创建mWindow,所以应该不会报这个错。
(2) Unable to add window -- token attrs.token is not valid; is your activity running?

     ```
        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            parentWindow = windowForClientLocked(null, attrs.token, false);
            if (parentWindow == null) {
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
            if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                    && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
        }
    ```

子window必须依赖于父window,并且父window不能是子window类型

  1. 检测window类型合法性

         AppWindowToken atoken = null;
         //系统window类型,parentWindow是null
         final boolean hasParent = parentWindow != null;
        //获取token,点击方法,里面是和hashmap集合,里面存储的是activity在启动的时候,创建的token,所  
        //以在这里获取到的是null,因为attrs里面taken为null,如果自己构造一个,其实也应该是null
         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;
    

经过上面的分析,我平时通过获取windowmanager,然后添加view的操作,应该都会进入token == null这个条件中,知道怎么才会报异常,那么接下来就知道怎么去应对了。当然,不同的Android系统版本,逻辑是有差异的,总得来说,系统版本越高,控制得越严格。具体的解决方案,请继续往下看,我会在最后讲解,如果只是想知道解决办法,可以直接拉到最后查看。

\color{red}{疑问:} 为什么Toast不会报异常:

  • Toast简单的源码分析
    Toast其实也是用的windowmanager添加我们view实现的,而且type是TYPE_TOAST,但是它为什么不会出现之前说的哪些异常呢,其实toast最大的不同就是,在toast添加window之前会先和windowmanagerservice进行通信,然后会返回一个binder对象(即token),然后在addwindow的时候一起带过去。下面就一起看看:
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

函数:
源码路径:enqueueToast(String pkg, ITransientNotification callback, int duration) frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            ....
            synchronized (mToastQueue) {
                ...
                    // If the package already has a toast, we update its toast
                    // in the queue, we don't move it to the end of the queue.
                    if (index >= 0) {
                        ...
                    } else {
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }
                    keepProcessAliveIfNeededLocked(callingPid);
                    ...
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show(record.token);
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                ...
        }
    }

关键代码: mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);

  mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
  //rameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
  LocalServices.addService(WindowManagerInternal.class, new LocalService());
  LocalService是WindowManagerService的内部类

   @Override
    public void addWindowToken(IBinder token, int type, int displayId) {
        WindowManagerService.this.addWindowToken(token, type, displayId);
    }

@Override
public void addWindowToken(IBinder binder, int type, int displayId) {
    if (!checkCallingPermission(MANAGE_APP_TOKENS, "addWindowToken()")) {
        throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
    }

    synchronized(mWindowMap) {
        final DisplayContent dc = mRoot.getDisplayContentOrCreate(displayId);
        WindowToken token = dc.getWindowToken(binder);
        if (token != null) {
            ...
            return;
        }
        if (type == TYPE_WALLPAPER) {
            new WallpaperWindowToken(this, binder, true, dc,
                    true /* ownerCanManageAppTokens */);
        } else {
            new WindowToken(this, binder, type, true, dc, true /* ownerCanManageAppTokens */);
        }

上面会将toast的token添加到DisplayContent里面,所以在上面获取的token的时候就不为null,这样就不会出现进入token == null时的异常,由于toast是维护在一个队列里面,下一个显示前,上一个时已经被移除,所以不会出现同时显示两个悬浮窗的情况,自然不会出现之前的说的异常。所以,可以模仿toast的队列,来防止同时弹处两个悬浮窗而导致的崩溃。

解决方案

我们来看一下,可以使用哪些系统类型,这里只列举常用的系统类型,类型实在太多了。根据if (!isSystemAlertWindowType(type)) 这个判断,其实我们可以把类型锁定到这些上:

  TYPE_PHONE,TYPE_PRIORITY_PHONE,TYPE_SYSTEM_ALERT,TYPE_SYSTEM_ERROR,TYPE_SYSTEM_OVERLAY,TYPE_APPLICATION_OVERLAY,TYPE_TOAST

这里面只有TYPE_TOAST不需要 "SYSTEM_ALERT_WINDOW"权限,但是在不同的版本会有不同的限制。

  • 用户已经授予了 "SYSTEM_ALERT_WINDOW"权限
  1. 系统版本 >= O
    这种情况下上面的所以类型按理都是可以使用的,但是TYPE_TOAST在targetSdkVersion >= 26时,是不能直接添加window的,而且在sdk 26后,google推荐使用TYPE_APPLICATION_OVERLAY,所以在有权限和系统版本在O或者以上时,可以用TYPE_APPLICATION_OVERLAY。
  2. 系统版本 < O
    这个时候就不能用TYPE_APPLICATION_OVERLAY,那么其实我们可以用TYPE_SYSTEM_ALERT。

\color{blue}{综上:}有权限的情况下,其实还是比较好处理的,这样就不会出现用TYPE_TOAST时出现的异常

  • 用户没有授予了 "SYSTEM_ALERT_WINDOW"权限
    这种情况下,没办法了,就不能用上面的系统类型了,那我们可以用TYPE_TOAST,这个类型是不需要特殊权限的,使用TYPE_TOAST有两种方法,直接有系统的Toast类,二是还是像上面那样,只是类型指定为TYPE_TOAST。
  1. 问题:
    使用TYPE_TOAST,在android8.0及以上,是不能直接添加window,在Android8.0以下的某些版本,在上一个没有移除前,是不能继续添加下一个的,当然可以模仿toast的方式,但是,这个需要hook系统的方法,所以可能存在兼容性问题,使用Toast,但是Toast显示时间有限制,而且默认是不接收触摸事件的,当然可以通过反射去修改。

分析完,感觉大脑已经缺氧了,这里说一解决思路:
\color{blue}{思路:}

  • 有SYSTEM_ALERT_WINDOW权限,请看上面。
  • 没有SYSTEM_ALERT_WINDOW权限。
    1. 如果需求比较简单,其实可以使用系统的Toast
    2. android O以下,可以使用TYPE_TOAST,但是要保证上一个window移除后才能添加下一个。
    3. 通过反射修改Toast属性,比如显示时长,接收触摸事件,这样有没有权限或者不同的版本,都是ok的,当然得注意一下Android9.0。
    4. 模仿Toast,自己创建token对象,hook系统类(比如WindowManagerService,LocalServices等系统类,获取WindowManagerService对象),加入到对应的数组里面
    5. 在addview处加try{}catch{},其实Toast在addview的时候都使用了try{}catch{}的
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345