近日,关注了一下项目的头部崩溃,崩溃量最大的是一个 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 崩溃时候的信息,抛到自己的后台中,可以很方便的定位问题。