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 崩溃时候的信息,抛到自己的后台中,可以很方便的定位问题。
    原文作者:ZDCrazy
    原文地址: https://www.jianshu.com/p/5377df40941a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞