一、概述
1.1 引言
一般的空指针问题, 往往是在多线程并发的情况下, 某个或多个临界资源多线程并发读写导致异常的发生,但是下面的问题是发生在单线程之中,引起了system重启。
1.2 错误Log
Process: system_server
Build: xiaomi/vince/vince:7.1.2/N2G47H/7.10.17:user/release-keys
10-17 17:03:23.800 1521 1669 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: xx.fg
10-17 17:03:23.800 1521 1669 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean xx.app.AlertDialog.isChecked()' on a null object reference
10-17 17:03:23.800 1521 1669 E AndroidRuntime: at com.xx.server.XxCompatModePackages$3.onClick(XxCompatModePackages.java:474)
10-17 17:03:23.800 1521 1669 E AndroidRuntime: at com.xx.internal.app.AlertControllerImpl$ButtonHandler.handleMessage(SourceFile:178)
10-17 17:03:23.800 1521 1669 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:102)
10-17 17:03:23.800 1521 1669 E AndroidRuntime: at android.os.Looper.loop(Looper.java:163)
10-17 17:03:23.800 1521 1669 E AndroidRuntime: at android.os.HandlerThread.run(HandlerThread.java:61)
10-17 17:03:23.800 1521 1669 E AndroidRuntime: at com.android.server.ServiceThread.run(ServiceThread.java:46)
1.3 初步分析
XxCompatModePackages.java中发生了NPE,立刻去看一下XxCompatModePackages.java中的代码。
465 private void createDialog() {
466 mAlertDialog = new AlertDialog.Builder(mContext)
467 .setTitle(R.string.xx_screen_ratio_hint)
468 .setMessage(R.string.xx_screen_ratio_hint_message)
469 .setCheckBox(true, mContext.getResources()
470 .getString(R.string.xx_screen_ratio_hint_dont_show_again))
471 .setPositiveButton(R.string.xx_screen_ratio_hint_ok, new DialogInterface.OnClickListener() {
472 @Override
473 public void onClick(DialogInterface dialog, int which) {
474 Message.obtain(mHandler, MSG_DONT_SHOW_AGAIN, mAlertDialog.isChecked()).sendToTarget();
475 }
476 })
477 .setNeutralButton(R.string.xx_screen_ratio_hint_go_to_settings, new DialogInterface.OnClickListener() {
478 @Override
479 public void onClick(DialogInterface dialog, int which) {
480 Message.obtain(mHandler, MSG_DONT_SHOW_AGAIN, mAlertDialog.isChecked()).sendToTarget();
481 gotoMaxAspectSettings();
482 }
483 })
484 .create();
485
486 mAlertDialog.setCanceledOnTouchOutside(false);
487 mAlertDialog.getWindow().getAttributes().type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
488
489 mAlertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
490 @Override
491 public void onDismiss(DialogInterface dialog) {
492 mAlertDialog = null;
493 }
494 });
495 }
474行发生了NPE,那只有mAlertDialog为NULL了,搜索mAlertDialog可以赋值为NULL的情况,mAlertDialog唯一被置空的机会是在mAlertDialog.setOnDismissListener的时候,且mAlertDialog用private修饰,也不存在外部修改的可能。
489 mAlertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
490 @Override
491 public void onDismiss(DialogInterface dialog) {
492 mAlertDialog = null;
493 }
494 });
createDialog方法在handleOnAppLaunch中调用的
450 private void handleOnAppLaunch(String packageName) {
451 if (!isRestrictAspect(packageName) && getDefaultAspectType(packageName) == CustomizeUtil.TYPE_SUGGEST) {
452 try {
453 Slog.i(TAG, "launching suggest app: " + packageName); //输出launching suggest app日志
454 if (mAlertDialog == null) {
455 createDialog();
456 }
457
458 mAlertDialog.show();
459 } catch (Exception e) {
460 Slog.e(TAG, "error showing suggest dialog", e);
461 }
462 }
463 }
mAlertDialog为null的时候就创建这个AlertDialog,不为null,直接show出来。注意每次在show的时候,都打印了“launching suggest app”这样一行log。这个时候只有在去结合日志分析FC的上下文了。
搜索日志中的launching suggest app
10-17 17:03:20.738 1521 1669 I XxCompatModePackages: launching suggest app: com.baidu.BaiduMap
10-17 17:03:20.811 1521 1738 D BaseXxPhoneWindowManager: keyCode:117 down:true eventTime:761166 downTime:761166 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:20.812 1521 1738 D BaseXxPhoneWindowManager: keyCode:117 down:false eventTime:761168 downTime:761168 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.018 1521 1738 D BaseXxPhoneWindowManager: keyCode:19 down:true eventTime:761373 downTime:761373 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.020 1521 1738 D BaseXxPhoneWindowManager: keyCode:19 down:false eventTime:761375 downTime:761375 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.121 1521 1738 D BaseXxPhoneWindowManager: keyCode:95 down:true eventTime:761476 downTime:761476 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.122 1521 1738 D BaseXxPhoneWindowManager: keyCode:95 down:false eventTime:761477 downTime:761477 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.226 1521 1738 I SplashScreenServiceDelegate: Empty check list, check all
10-17 17:03:21.228 1521 1738 I SplashScreenServiceDelegate: requestSplashScreen 2ms, com.xx.xxbbs
10-17 17:03:21.229 1521 1738 I ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.xx.xxbbs/.activity.WelcomeActivity} from uid 0 on display 0
10-17 17:03:21.238 2541 2776 I WtProcessController: mCurTask:13
10-17 17:03:21.275 1521 1737 I ActivityManager: Start proc 7289:com.xx.xxbbs/u0a120 for activity com.xx.xxbbs/.activity.WelcomeActivity caller=null
10-17 17:03:21.363 1521 1737 D BaseXxPhoneWindowManager: keyCode:22 down:true eventTime:761718 downTime:761718 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.365 1521 1737 D BaseXxPhoneWindowManager: keyCode:22 down:false eventTime:761720 downTime:761720 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.383 2541 2776 I WtProcessController: MOVE TO FOREGROUND: com.xx.xxbbs 10120
10-17 17:03:21.384 2541 2776 I StatusController: Last foreground:com.baidu.BaiduMap uid:10136 Current foreground:com.xx.xxbbs uid:10120
10-17 17:03:21.384 2541 2776 I WtProcessController: FOREGROUND INFO: name=com.xx.xxbbs uid=10120 pid=7289 TaskId:13
10-17 17:03:21.413 1521 1538 V UidProcStateHelper: process state changed:[7289,10120,2]
10-17 17:03:21.425 1521 1537 I ActiveServicesInjector: Low priority start of: ServiceRecord{1bce22 u0 com.xiaomi.market/.service.AppActiveStatService}
10-17 17:03:21.574 1521 1738 I SplashScreenServiceDelegate: Empty check list, check all
10-17 17:03:21.576 1521 1738 I SplashScreenServiceDelegate: requestSplashScreen 2ms, com.xiaomi.scanner
10-17 17:03:21.576 1521 1738 I ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.xiaomi.scanner/.app.ScanActivity} from uid 0 on display 0
10-17 17:03:21.583 2541 2776 I WtProcessController: mCurTask:14
10-17 17:03:21.695 1521 1738 D BaseXxPhoneWindowManager: keyCode:136 down:true eventTime:762050 downTime:762050 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.697 1521 1738 D BaseXxPhoneWindowManager: keyCode:136 down:false eventTime:762052 downTime:762052 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.798 1521 1738 D BaseXxPhoneWindowManager: keyCode:20 down:true eventTime:762153 downTime:762153 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.799 1521 1738 D BaseXxPhoneWindowManager: keyCode:20 down:false eventTime:762154 downTime:762154 policyFlags:2b000000 deviceId:-1 isScreenOn:true keyguardActive:false repeatCount:0
10-17 17:03:21.928 1521 1669 I XxCompatModePackages: launching suggest app: com.xx.xxbbs
发现了不少"launching suggest app",有两次时间相差约1秒(第一条日志和最后一条日志),也就是说连续两次调用了handleOnAppLaunch方法?初步猜测是第一次创建了mAlertDialog之后,monkey点击了取消,触发了setOnDismissListener,但是onDismiss还没有走完(通过Handler发送Msg),紧接着第二次handleOnAppLaunch调用,这时候mAlertDialog不为null,不用创建,直接show出来,用户再次点击对话框的button触发setPositiveButton方法,此时第一次onDismiss刚好走完,发生了NP。但是我又想一想,虽然是前后弹了两次Dialog,但是Dialog的创建和dismiss是在同一个线程中,主线的Message是一个接着一个处理的,理论上不会发生上面的问题。
二、进一步分析
尝试写复现DEMO,很简单,我在onCreate中创建一个Dialog,然后在取消按钮中把mAlertDialog赋值为null。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createDialog();
}
private void createDialog() {
mAlertDialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("title")
.setMessage("message")
.setPositiveButton("确定", new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog, int which) {
boolean showing = mAlertDialog.isShowing();
Log.d("MainActivity","showing"+showing);
}
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAlertDialog = null;
}
}).create();
mAlertDialog.show();
}
如果是上面的代码,即使在同一个线程中也可能出现空指针的情况,现在先按住取消按钮不放,在按住确定按钮,然后两个同时松开,就可能出现空指针,基本上是一个必现的问题。继续按照XxCompatModePackages.java里面的逻辑写一个。
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createDialog();
}
private void createDialog() {
mAlertDialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("title")
.setMessage("message")
.setPositiveButton("确定", new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog, int which) {
boolean showing = mAlertDialog.isShowing();
Log.d("MainActivity","showing"+showing);
}
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("MainActivity","取消");
boolean showing = mAlertDialog.isShowing();
}
}).setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
Log.d("MainActivity","onDismiss");
mAlertDialog = null;
}
}).create();
mAlertDialog.show();
}
这次怎么也复现不了了,还是先看下代码吧。
三、深入分析
现在梳理一下Dialog的setPositiveButton和setNegativeButton流程,以setPositiveButton为例。
609 public Builder setPositiveButton(@StringRes int textId, final OnClickListener listener) {
610 P.mPositiveButtonText = P.mContext.getText(textId);
611 P.mPositiveButtonListener = listener;
612 return this;
613 }
P是AlertParams对象,将所设置的文本和listener保存在对象P的mPositiveButtonText和mPositiveButtonListener中,什么时候会用呢,Dialog在onCreate中会调用apply,apply会来取这个参数。
996 public void apply(AlertController dialog) {
997 .....
1016 if (mPositiveButtonText != null) {
1017 dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
1018 mPositiveButtonListener, null);
1019 }
1020 if (mNegativeButtonText != null) {
1021 dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
1022 mNegativeButtonListener, null);
1023 }
1024 if (mNeutralButtonText != null) {
1025 dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
1026 mNeutralButtonListener, null);
1027 }
.....
1054 }
1055
继续看setButton的实现
339 public void setButton(int whichButton, CharSequence text,
340 DialogInterface.OnClickListener listener, Message msg) {
341
342 if (msg == null && listener != null) {
343 msg = mHandler.obtainMessage(whichButton, listener);
344 }
345
346 switch (whichButton) {
347
348 case DialogInterface.BUTTON_POSITIVE:
349 mButtonPositiveText = text;
350 mButtonPositiveMessage = msg;
351 break;
352
353 case DialogInterface.BUTTON_NEGATIVE:
354 mButtonNegativeText = text;
355 mButtonNegativeMessage = msg;
356 break;
357
358 case DialogInterface.BUTTON_NEUTRAL:
359 mButtonNeutralText = text;
360 mButtonNeutralMessage = msg;
361 break;
362
363 default:
364 throw new IllegalArgumentException("Button does not exist");
365 }
366 }
将文本text和msg保存在mButtonNeutralText和mButtonNeutralMessage中,最终用他们构造了一个监听器mButtonHandler。
125 private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
126 @Override
127 public void onClick(View v) {
128 final Message m;
129 if (v == mButtonPositive && mButtonPositiveMessage != null) {
130 m = Message.obtain(mButtonPositiveMessage);
131 } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
132 m = Message.obtain(mButtonNegativeMessage);
133 } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
134 m = Message.obtain(mButtonNeutralMessage);
135 } else {
136 m = null;
137 }
138
139 if (m != null) {
140 m.sendToTarget();
141 }
142
143 // Post a message so we dismiss after the above handlers are executed
144 mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
145 .sendToTarget();
146 }
147 };
无论是点击确定按钮还是取消按钮都需要设置这个监听器mButtonHandler,因为Dialog在show出来的时候,Dialog解析xml布局之后,里面的确定按钮和取消按钮的监听器就会被设置mButtonHandler。并且最后还发送一个类型为MSG_DISMISS_DIALOG的msg,为了在点击确定、取消、中立按钮时候能够默认关闭对话框。
梳理到这里就明白了上面DEMO1发生FC的原因,在我同时松开取消按钮与确定按钮的时候,事实上这个时候线程的MessageQueue里面有两个msg没有被处理,分别是取消按钮对应的Message和确定按钮对应的Message。当Looper来取这个消息的时候,先取的是取消按钮对应的Message,处理完后在取确定按钮对应的Message,但是我们取消按钮对应的监听器回调中把mAlertDialog赋值为null了,所以出现了NPE。那么对于DEMO2,如果存在消息队列进入了三个Message,包括Dialog确定按钮 onClick与取消按钮onClick对应的Message,以及onDismiss 对应的Message,并且onDismiss对应的Message在两个onClick 对应的Message之间,那么也可能产生NullPointerException。基于手动难以复现的情况下,只有跑monkey了,结果每次都很快出现了NullPointerException。
// CRASH: onekeymonkey.wangjing.com.onekeymonkey (pid 11918)
// Short Msg: java.lang.NullPointerException
// Long Msg: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.app.Dialog.isShowing()' on a null object reference
// Build Label: Xiaomi/jason/jason:7.1.1/NMF26X/7.11.24:user/release-keys
// Build Changelist: 7.11.24
// Build Time: 1511465058000
// java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.app.Dialog.isShowing()' on a null object reference
// at onekeymonkey.wangjing.com.onekeymonkey.MainActivity$4.onClick(MainActivity.java:82)
// at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:166)
// at android.os.Handler.dispatchMessage(Handler.java:102)
// at android.os.Looper.loop(Looper.java:163)
// at android.app.ActivityThread.main(ActivityThread.java:6384)
// at java.lang.reflect.Method.invoke(Native Method)
// at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
// at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:791)
//
** Monkey aborted due to error.
四、结论与修复
以后出现了空指针的情况,在一个线程的情况下,需要确定一下是否使用了Handler消息机制,对于我们这个问题,知道了rootcase,那么这里直接判断空就可以了。