Window type can not be changed issue

   近日,关注了一下项目的头部崩溃,崩溃量最大的是一个 Window 相关问题,日均崩溃次数接近了快 1W 次,并且崩溃被 keep 了快2年之久都没解掉,崩溃日志如下:

----exception localized message----
Window type can not be changed after the window is added.

----exception stack trace----
java.lang.IllegalArgumentException: Window type can not be changed after the window is added.
    at android.os.Parcel.readException(Parcel.java:1688)
    at android.os.Parcel.readException(Parcel.java:1637)
    at android.view.IWindowSession$Stub$Proxy.relayout(IWindowSession.java:985)
    at android.view.ViewRootImpl.relayoutWindow(ViewRootImpl.java:6249)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2220)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1716)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6903)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:874)
    at android.view.Choreographer.doCallbacks(Choreographer.java:686)
    at android.view.Choreographer.doFrame(Choreographer.java:621)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:860)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:163)
    at android.app.ActivityThread.main(ActivityThread.java:6228)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
-----dumpkey----

   由于崩溃日志中不包含项目的任何信息,所以从崩溃日志中,无法定位到代码哪一处造成的崩溃,那么首先来找该崩溃日志出现的地方吧,崩溃出现在 WindowManagerService 的 relayoutWindow() 方法中,崩溃处代码如下:

if (win.mAttrs.type != attrs.type) {
                    throw new IllegalArgumentException(
                            "Window type can not be changed after the window is added.");
                }

   即 ViewRootImpl 调用 relayoutWindow() 过程中,由于该窗口之前已经被添加过了,但是再此后,又尝试去改变窗口的 type 类型,WMS 就会返回一个崩溃出来。但是项目用到 window 的地方这么多,如何去定位到哪一处导致的问题呢?
   经过漫长的思索尝试,终于找到了一种可行的定位该问题的方案,在下一版本发出去的时候,可喜的发现竟然很快找到了出问题的地方,在半个小时的修改后,再发版本出去,被keep watch 快2年之久的崩溃被解掉了!那么是如何定位到问题的呢?
   答案就是 hook WindowManagerGlobal , 在添加一个window 的过程中,首先一定会走到 WindowManagerGlobal addView() 方法中,该方法代码如下:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
       ...
       ...
       root = new ViewRootImpl(view.getContext(), display);
       view.setLayoutParams(wparams);
       mViews.add(view);  // mViews 集合保存各个window 对应的根 view 集合
       mRoots.add(root);
       mParams.add(wparams); 
}

  从代码里面可以看到,在 addView()时候,最终会把该 window 对应的根 view 以及 windowParams 都存放到对应的集合里面去,由于 WindowManagerGlobal 是一个单例对象,所以只要是在同一个进程中,通过反射 mViews 集合以及 mParams 集合,就可以得到该进程中所有的 window 信息,包括 windowType , rootView 等等。现在只需要在各个添加window 的地方,给 rootView 添加一个 Tag 信息,Tag 包含的信息可以自己指定,解决该 Bug 时,收集的 Tag 信息包含添加时,对应的时间戳,className , 以及添加时的 window Type , 代码如下:

/**
     * @param className
     * @param paramType
     * @param currentTimeMillis
     * @param isSPNeed          need write to SP?
     * @return
     */
    public static String generateViewTag(String className, int paramType, long currentTimeMillis, boolean isSPNeed) {

        String tagBuilder = className + " : " +
                paramType + " : " +
                currentTimeMillis;

        if (isSPNeed) {
            // in case that commonLib has not been initialized
            try {
                CommonLibrary.getIns().getIPref().putString("last_add_window_tag", tagBuilder);
            } catch (Exception ignored) {

            }
        }

        return tagBuilder;
    }

  接下来就是在项目崩溃的时候,去hook WindowManagerGlobal,拿到所有的 window 信息,前后进行比对,发现 windowType 不一样的地方,解析viewTag , 得到添加时候的 Class 信息,就可以知道是哪个Class 在 addView() 的时候出了问题 。 hook 相关代码如下:

/**
 * The interface is align with API 21
 */
public class WindowManagerGlobalHack {

    private Object sDefaultWindowManager;

    public static WindowManagerGlobalHack getInstance() throws ReflectionUtils.ReflectionException {
        WindowManagerGlobalHack instance = new WindowManagerGlobalHack();
        Object sDefaultWindowManager = ReflectionUtils.Hack("android.view.WindowManagerGlobal")
                .call("getInstance");

        // attach to wrap object
        instance.sDefaultWindowManager = sDefaultWindowManager;
        return instance;
    }
   
    private Object getLock() {

        try {
            Field lockField = WindowManagerGlobalHack.getDeclaredField(sDefaultWindowManager, "mLock");
            lockField.setAccessible(true);
            return lockField.get(sDefaultWindowManager);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * get view list from WindowManagerGlobal
     *
     * @return
     * @see WindowManagerGlobal addView();
     */
    public ArrayList<View> getViewInWMGlobal() {
        try {
            Object obj = getLock();
            if (obj == null) {
                return null;
            }
            synchronized (obj) {
                Field viewField = WindowManagerGlobalHack.getDeclaredField(sDefaultWindowManager, "mViews");
                viewField.setAccessible(true);
                ArrayList<View> viewList = new ArrayList<View>((ArrayList<View>) viewField.get(sDefaultWindowManager));
                return viewList;
            }
        } catch (Exception ignored) {

        }
        return null;
    }

    /**
     * get view list from WindowManagerGlobal
     *
     * @return
     * @see WindowManagerGlobal addView();
     */
    public ArrayList<WindowManager.LayoutParams> getParamsListInWMGlobal() {
        try {
            Object obj = getLock();
            if (obj == null) {
                return null;
            }
            synchronized (obj) {
                Field paramsField = WindowManagerGlobalHack.getDeclaredField(sDefaultWindowManager, "mParams");
                paramsField.setAccessible(true);
                ArrayList<WindowManager.LayoutParams> paramList = new ArrayList<WindowManager.LayoutParams>((ArrayList<WindowManager.LayoutParams>) paramsField.get(sDefaultWindowManager));
                return paramList;
            }
        } catch (Exception ignored) {

        }
        return null;
    }

    /**
     * 循环向上转型, 获取对象的 DeclaredField
     *
     * @param object    : 子类对象
     * @param fieldName : 父类中的属性名
     * @return 父类中的属性对象
     */
    public static Field getDeclaredField(Object object, String fieldName) {
        Field field = null;

        for (Class<?> clazz = object.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
            try {
                field = clazz.getDeclaredField(fieldName);
                return field;
            } catch (Exception e) {
                //这里甚么都不要做!并且这里的异常必须这样写,不能抛出去。
                //如果这里的异常打印或者往外抛,则就不会执行clazz = clazz.getSuperclass(),最后就不会进入到父类中了

            }
        }
        return null;
    }

    /**
     * @param className
     * @param paramType
     * @return
     */
    public static String generateViewTag(String className, int paramType) {
        return generateViewTag(className, paramType, System.currentTimeMillis());
    }


    /**
     * @param className         simpleClassName
     * @param paramType
     * @param currentTimeMillis the time window has been added
     * @return
     * @see WindowManager.LayoutParams
     */
    public static String generateViewTag(String className, int paramType, long currentTimeMillis) {
        return generateViewTag(className, paramType, currentTimeMillis, true);
    }


    /**
     * @param className
     * @param paramType
     * @param currentTimeMillis
     * @param isSPNeed          need write to SP?
     * @return
     */
    public static String generateViewTag(String className, int paramType, long currentTimeMillis, boolean isSPNeed) {

        String tagBuilder = className + " : " +
                paramType + " : " +
                currentTimeMillis;

        if (isSPNeed) {
            // in case that commonLib has not been initialized
            try {
                CommonLibrary.getIns().getIPref().putString("last_add_window_tag", tagBuilder);
            } catch (Exception ignored) {

            }
        }

        return tagBuilder;
    }
}

  上述说的是 App 崩溃的时候去收集信息,那么自己需要写一个 CrashHandler 继承自 UncaughtExceptionHandler 即可,如果 App 有发生崩溃,那么在 uncaughtException() 回调中,可以拦截掉信息,然后自己可以在该方法做对应处理。比如手动抛日志到自己的后台等等。收集日志方法如下:

private void appendWindowInfoIfNeed(FileWriter fw) throws IOException {
        if (mDumpKey.equals("3548080566") || mDumpKey.equals("1121291690")) {
            try {
               // 通过反射拿到崩溃时的 window 信息。
               // 解析 viewList 的viewTag ,代表添加时刻的 window 信息
               // 将崩溃时和添加时 windowType 进行比对,找到不一致的地方即可
                ArrayList<View> viewList = WindowManagerGlobalHack.getInstance().getViewInWMGlobal();
                ArrayList<WindowManager.LayoutParams> paramsList = WindowManagerGlobalHack.getInstance().getParamsListInWMGlobal();

                if (viewList == null || paramsList == null) {
                    fw.write("\n WMG get list exception");
                    return;
                }

                fw.write("\n WMG viewSize : " + viewList.size() + " , paramSize : " + paramsList.size());

                for (int i = 0; i < viewList.size(); i++) {

                    View view = viewList.get(i);
                    WindowManager.LayoutParams layoutParams = paramsList.get(i);

                    String viewTag = view.getTag() == null ? "" : view.getTag().toString();
                    int paramsType = layoutParams.type;

                    fw.write("\n view Tag : " + viewTag + " , params type : " + paramsType);
                }

                fw.write("\n last window tag : " + GlobalPref.getIns().getLastAddWindowTag());

            } catch (Exception ignored) {}
        }
    }

  通过对上述信息的收集,在该版本发出去的时候,观察收上来的日志信息,果然发现有一个window Type 被改变了,截图如下:

 WMG viewSize : 1 , paramSize : 1
 view Tag : notification.d : 2002 : 1527281256386 , params type : 2005
 last window tag : notification.d : 2002 : 1527281256386
 samsung_prob_time : 
 cover_dialog_time : 0

----Pref Info----
count : 64

----exception localized message----
Window type can not be changed after the window is added.

----exception stack trace----
java.lang.IllegalArgumentException: Window type can not be changed after the window is added.
    at android.os.Parcel.readException(Parcel.java:1688)
    at android.os.Parcel.readException(Parcel.java:1637)
    at android.view.IWindowSession$Stub$Proxy.relayout(IWindowSession.java:953)
    at android.view.ViewRootImpl.relayoutWindow(ViewRootImpl.java:5737)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1776)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1272)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6408)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:874)
    at android.view.Choreographer.doCallbacks(Choreographer.java:686)
    at android.view.Choreographer.doFrame(Choreographer.java:621)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:860)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6165)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:888)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:778)
-----dumpkey----
dumpkey=3548080566

  可以看到,notification.d 这个类,添加的 windowType 由原始的 TYPE_PHONE 变为了 TYPE_TOAST , 哈哈,万事大吉,通过对该类的分析,发现程序写的确实有一点问题。在下一版解掉之后,崩溃问题自然而然就消失了!
  综上所述,解决该问题,用的技术也比较简单,可以简单归为以下三点:

  • 在 WindowManager.addView() 的时候,可以对view 添加自己想要的信息到 view 的 Tag 中。
  • 通过反射 WindowManagerGlobal ,可以轻易的拿到当前进程中所有的 window 相关信息,便于分析问题。
  • 通过 crashHandler 来收集 App 崩溃时候的信息,抛到自己的后台中,可以很方便的定位问题。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容