天天看點

一個Bug案例的解決過程:連續輸入錯誤的PIN碼,不能實作第二次倒計時30s才能重試

此問題是草稿箱存了兩年的一篇文章,還是重新發表了吧……^.^

當時新工作的第一個Bug,挺有紀念意義的,是以寫下總結。

問題的現象:

1.打開 Settings → Security →Screen lock,設定PIN。

2.重新打開該選項,輸入錯誤的PIN五次,手機會開始提示30s後才能繼續嘗試。

3.等待30s後,再次輸入錯誤的PIN五次,觀察現象。

預期結果:

步驟3之後的效果和步驟2之後的效果一樣,需要30s之後才能重新嘗試輸入。

實際結果:

步驟3之後,點選continue按鈕無反應,輸入框裡仍可以繼續輸入。

複現機率:

5/5

問題分析過程:

一.

猜想是代碼邏輯有問題,是以首先找到該頁面的實作代碼。

根據頁面關鍵字串找到該頁面的實作邏輯在:packages/apps/Settings/src/com/android/settings/ConfirmLockPassword.java類中:

二.

頁面點選事件的處理如下:

public void onClick(View v) {
            if (getActivity() == null) return;

            switch (v.getId()) {
                case R.id.next_button:  //點選CONTINUE按鈕
                    handleNext();
                    break;

                case R.id.cancel_button:    //點選CANCEL按鈕
                    getActivity().setResult(RESULT_CANCELED);
                    getActivity().finish();
                    break;
            }
        }
           

其中handleNext()定義如下:

private void handleNext() {
            final String pin = mPasswordEntry.getText().toString();
            if (mLockPatternUtils.checkPassword(pin)) { //輸入正确的PIN之後的處理

                Intent intent = new Intent();
                if (getActivity() instanceof ConfirmLockPassword.InternalActivity) {
                    intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE,
                                    mIsAlpha ? StorageManager.CRYPT_TYPE_PASSWORD
                                             : StorageManager.CRYPT_TYPE_PIN);
                    intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin);
                }

                getActivity().setResult(RESULT_OK, intent);
                getActivity().finish();
            } else {        //輸入的PIN錯誤
                if (++mNumWrongConfirmAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {    //連續錯誤次數大于等于5次,則啟動倒計時
                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
                    handleAttemptLockout(deadline);
                } else {    //連續輸入錯誤次數小于5次,則還可以繼續嘗試
                    showError(R.string.lockpattern_need_to_unlock_wrong);
                }
            }
        }
           

而 handleAttemptLockout()定義如下:

private void handleAttemptLockout(long elapsedRealtimeDeadline) {
    if (mCountdownTimer != null) {
        return;
    }
    long elapsedRealtime = SystemClock.elapsedRealtime();   //擷取系統目前時間
    showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, );
    mPasswordEntry.setEnabled(false);
    mCountdownTimer = new CountDownTimer(   
            elapsedRealtimeDeadline - elapsedRealtime,
            LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {

        @Override
        public void onTick(long millisUntilFinished) {  //此處是30s倒計時的處理
            final int secondsCountdown = (int) (millisUntilFinished / );
            mHeaderText.setText(getString(
                    R.string.lockpattern_too_many_failed_confirmation_attempts_footer,
                    secondsCountdown));
        }

        @Override
        public void onFinish() {    //30s倒計時完成
            mPasswordEntry.setEnabled(true);
            mHeaderText.setText(getDefaultHeader());
            mNumWrongConfirmAttempts = ;
        }
    }.start();
}
           

每當我們連續輸入5次錯誤的PIN之後,程式就會進入這個方法進行處理。

那麼第一次輸入5次錯誤PIN的時候,進入此方法,首先會判斷 mCountdownTimer是否為null。在此類中可以找到mCountdownTimer的聲明:

private CountDownTimer mCountdownTimer;
           

在這裡mCountdownTimer預設初始化為null,其他地方并未初始化。是以mCountdownTimer != null判斷失敗,此方法繼續執行。

接下來,建立了一個新的用于倒計時30s的對象,并開始倒計時。

倒計時結束後,輸入框恢複為可輸入狀态。這時再輸入錯誤的PIN五次後,點選CONTINUE按鈕,流程又會進入 handleAttemptLockout()方法中。

此時由于mCountdownTimer對象已經被建立了,并不為null,是以直接執行了return()。

繼續點選CONTINUE,又進入handleAttemptLockout()中,還是return。是以再點選CONTINUE都沒反應了。

三.

知道問題發生的原因之後,我一開始改動想法是這樣:

既然第二次輸入錯誤5次之後,mCountdownTimer對象已經建立了,這時候要啟動倒計時,隻要直接讓mCountdownTimer重新執行start()方法就好了。

于是改成如下進行測試:

if (mCountdownTimer != null) {
                showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, );
                mPasswordEntry.setEnabled(false);
         mCountdownTimer.start();
                return;
            }
           

測試結果:第二次連續輸入5次錯誤PIN之後,果然倒計時可以正常進行了。

但是經多次測試,發現有時候倒計時并不是30s,有時候從十幾秒,有時候從二十幾秒開始倒計時。這是什麼原因呢?

經過打log追蹤規律,終于發現:如果在倒計時未完成的過程中退出此頁面,下次重新輸入5次PIN錯誤之後,倒計時就會不正常。

原因如下:

倒計時過程中如果退出該頁面,則會執行onPause()方法:

@Override
        public void onPause() {
            super.onPause();
            mKeyboardView.requestFocus();
            if (mCountdownTimer != null) {
                mCountdownTimer.cancel();
                mCountdownTimer = null;
            }
        }
           

可以知道,如果退出此頁面的話, mCountdownTimer就會被置為null。但是要注意倒計時還在繼續,cancel()方法并不會終止倒計時的進行。

而再次進入該頁面的時候,會執行onResume()方法:

@Override
        public void onResume() {
            // TODO Auto-generated method stub
            super.onResume();
            mKeyboardView.requestFocus();
            long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
            if (deadline != ) {    //倒計時未完成
                handleAttemptLockout(deadline);
            } else {
                mPasswordEntry.setEnabled(true);
                mHeaderText.setText(getDefaultHeader());
                mNumWrongConfirmAttempts = ;
            }
        }
           

可以看出,如果重新進入此頁面的話,如果倒計時仍未完成,則又執行 handleAttemptLockout()方法,傳入的參數 deadline即為倒計時截止時的時間。

此時由于mCountdownTimer已經在onPause()方法中被置為null,是以執行handleAttemptLockout()方法後會建立一個新的mCountdownTimer,而根據傳入的構造參數看,這個mCountdownTimer開始倒計時的時間就是剩餘的倒計時時間,至此流程都是正常的。

但是如果這次倒計時完成後,又接着輸入五次錯誤的PIN碼之後,由于上次建立的mCountdownTimer此時不為null,是以本次會直接執行此對象的start()方法開始倒計時。但是由于上次建立對象時傳入的倒計時時間參數不是30s,是以這次倒計時也不會從30s開始倒計時,而是從和上次進入倒計時頁面時的剩餘時間一樣的時間開始,問題就是這樣出現了。

是以之前的改法存在問題,得另找方法。

四.

既然mCountdownTimer有時候會被置為null,有時候又會被建立新對象。不如每次倒計時完成都置為null,每次需要倒計時再建立新對象。

為實作此目的,把handleAttemptLockout()方法做一行改動:

if (mCountdownTimer != null) {
                mCountdownTimer = null;
//                return;
            }
           

就是把原來的return改為了mCountdownTimer = null。

改動之後的方法如下:

private void handleAttemptLockout(long elapsedRealtimeDeadline) {
            if (mCountdownTimer != null) {
                mCountdownTimer = null;
            }
            long elapsedRealtime = SystemClock.elapsedRealtime();
            showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, );
            mPasswordEntry.setEnabled(false);
            mCountdownTimer = new CountDownTimer(
                    elapsedRealtimeDeadline - elapsedRealtime,
                    LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {

                @Override
                public void onTick(long millisUntilFinished) {
                    final int secondsCountdown = (int) (millisUntilFinished / );
                    mHeaderText.setText(getString(
                            R.string.lockpattern_too_many_failed_confirmation_attempts_footer,
                            secondsCountdown));
                }

                @Override
                public void onFinish() {
                    mPasswordEntry.setEnabled(true);
                    mHeaderText.setText(getDefaultHeader());
                    mNumWrongConfirmAttempts = ;
                }
            }.start();
        }
           

改動之後,

對于handleAttemptLockout()方法,流程走到這個方法時,有兩種可能,

1.上次倒計時建立的mCountdownTimer此時已經倒計時完成,繼續輸入5次錯誤的PIN後執行此方法,此時mCountdownTimer不為null。

2.上次倒計時建立的mCountdownTimer倒計時過程中,頁面退出,重新打開此頁面時onResume()方法中又執行了此方法,但mCountdownTimer在頁面退出前onPause()方法中已被置為null。

對于第一種可能,mCountdownTimer會被置為null,然後建立一個新的mCountdownTimer開始倒計時。

對于第二種可能,流程和改動之前無差别,會直接建立新的mCountdownTimer開始倒計時。

五.

上面的改動,經測試是可以解決問題的。但是對流程影響較大,有潛在的風險。是以思考之後,進一步優化,改為如下方案:

private void handleAttemptLockout(long elapsedRealtimeDeadline) {
            if (mCountdownTimer != null) {
                return();//這裡不動
            }
            long elapsedRealtime = SystemClock.elapsedRealtime();
            showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, );
            mPasswordEntry.setEnabled(false);
            mCountdownTimer = new CountDownTimer(
                    elapsedRealtimeDeadline - elapsedRealtime,
                    LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {

                @Override
                public void onTick(long millisUntilFinished) {
                    final int secondsCountdown = (int) (millisUntilFinished / );
                    mHeaderText.setText(getString(
                            R.string.lockpattern_too_many_failed_confirmation_attempts_footer,
                            secondsCountdown));
                }

                @Override
                public void onFinish() {
                    mCountdownTimer = null;//改到這裡
                    mPasswordEntry.setEnabled(true);
                    mHeaderText.setText(getDefaultHeader());
                    mNumWrongConfirmAttempts = ;
                }
            }.start();
        }
           

即将mCountdownTimer = null放到了onFinish()方法中,使得每次倒計時完成時,mCountdownTimer會被置為null。

與上次的改動相比:上次的是在每次倒計時開始之前,将上次的mCountdownTimer置為null。而本次改動是将mCountdownTimer置為null的操作放在了mCountdownTimer倒計時完成之後。這樣改動,流程更加合理清楚,風險也更得以避免。

總結:

1.改Bug的時候,一定先要理清流程,對于各種流程的可能性都要考慮到,必要的時候可以通過畫流程圖,分條列出等方式進行分析。

2.改動之後,充分驗證,一般容易忽略的驗證比如橫豎屏切換,點選Back或者Home傳回又重新進入,以及重複多次驗證等都要經過測試。

3.在所有可行的方案中,選擇對原有流程影響最小,最安全的。一次改動最好隻解決一個問題。