應用與系統穩定性第四篇---單線程導致的空指針問題分析

一、概述

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,那么這里直接判斷空就可以了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容

  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,482評論 0 17
  • 目錄介紹 1.簡單用法 2.AlertDialog源碼分析2.1 AlertDialog.Builder的構造方法...
    楊充211閱讀 1,130評論 1 1
  • 前言 在Android開發中,消息推送功能的使用非常常見。 推送消息截圖 為了降低開發成本,使用第三方推送是現今較...
    BillyLu1994閱讀 4,458評論 0 2
  • 明知穿腸毒, 笑飲可敬無?
    佘洛閱讀 197評論 0 0
  • Ennnnnnnn,游戲,白眼,無聊加,困 話說錯了別的游戲都好有意思的 嚶嚶嚶。嘔。惡心自己一波。 啦啦啦啦啦啦...
    聽說_b482閱讀 147評論 0 0