此問題是草稿箱存了兩年的一篇文章,還是重新發表了吧……^.^
當時新工作的第一個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.在所有可行的方案中,選擇對原有流程影響最小,最安全的。一次改動最好隻解決一個問題。