背景描述

客户反馈售后问题,设置pin码解锁后,锁屏解锁,输入1234,界面显示4**4,能正常解锁。

问题分析

分析pin码解锁界面点击事件和密码框显示逻辑:
pin码显示界面键盘和密码框为自定义view,非系统软键盘和基础控件EditText。
界面中点击事件下发、密码框显示数字、数字过几秒变为*等逻辑,均为自定义控件内部逻辑。
关键类:

SystemUI/src/com/android/keyguard/PasswordTextView.java
SystemUI/src/com/android/keyguard/NumPadKey.java
PasswordTextView

SystemUI/src/com/android/keyguard/PasswordTextView.java中

//用户点击数字的对象列表,每一个显示的数字都是一个CharState,CharState负责记录点击的值和从数字变为*的动画
private ArrayList<CharState> mTextChars = new ArrayList<>();
//密码拼接字符串,用来解锁时验证密码是否和设置的pin码匹配
private String mText = "";
//栈,用来缓存CharState对象,在用户频繁输入删除时,防止每次点击都new对象
private Stack<CharState> mCharPool = new Stack<>();

密码显示的绘制

protected void onDraw(Canvas canvas) {
...
int length = mTextChars.size();
for (int i = 0; i < length; i++) {
    CharState charState = mTextChars.get(i);
    float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition, 
    charLength);
    currentDrawPosition += charWidth;
}
...
}

charState.draw里面绘制具体的数字或者·

public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition,
        float charLength) {
    boolean textVisible = currentTextSizeFactor > 0;
    boolean dotVisible = currentDotSizeFactor > 0;
    float charWidth = charLength * currentWidthFactor;
    if (textVisible) {//绘制数字
        float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor
                + charHeight * currentTextTranslationY * 0.8f;
        canvas.save();
        float centerX = currentDrawPosition + charWidth / 2;
        canvas.translate(centerX, currYPosition);
        canvas.scale(currentTextSizeFactor, currentTextSizeFactor);
        canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint);
        canvas.restore();
    }
    if (dotVisible) {//绘制·
        canvas.save();
        float centerX = currentDrawPosition + charWidth / 2;
        canvas.translate(centerX, yPosition);
        canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint);
        canvas.restore();
    }
    return charWidth + mCharPadding * currentWidthFactor;
}

点击事件
NumPadKey是0-9的数字,点击事件调用PasswordTextView的append方法
NumPadKey中:

if (mTextView != null && mTextView.isEnabled()) {
    mTextView.append(Character.forDigit(mDigit, 10));
}

PasswordTextView中:

public void append(char c) {
    int visibleChars = mTextChars.size();
    CharSequence textbefore = getTransformedText();
    mText = mText + c;//字串拼接
    int newLength = mText.length();
    CharState charState;
    if (newLength > visibleChars) {
        charState = obtainCharState(c);//获取CharState对象
        mTextChars.add(charState);//添加到列表中
    } else {
        charState = mTextChars.get(newLength - 1);
        charState.whichChar = c;
    }
    charState.startAppearAnimation();
    ...
}
//mCharPool维护一个栈,防止输入删除频繁创建对象
    private CharState obtainCharState(char c) {
        CharState charState;
        if(mCharPool.isEmpty()) {
            charState = new CharState();
        } else {
            charState = mCharPool.pop();
            charState.reset();
        }
        charState.whichChar = c;
        return charState;
    }
Log分析

在流程中添加log,打印onDraw方法和append方法和obtainCharState方法
1、onDraw方法中log

01-05 12:04:52.780009  1823  1823 W xiaohe  : onDraw 333 mText: 12 mTextCharTmp: 12
01-05 12:04:52.841712  1823  1823 W xiaohe  : onDraw 333 mText: 123 mTextCharTmp: 323

mText和mTextChar的值不一致。
点击1、2、3,12的时候还是一致的,当点击3的时候显示为3*3
2、append方法中的log

01-05 12:04:52.834189  1823  1823 D xiaohe  : PasswordTextview append display:[CharState{whichChar=3,textAnimationIsGrowing=false,dotAnimationIsGrowing=true,currentTextSizeFactor=0.0}, CharState{whichChar=2,textAnimationIsGrowing=true,dotAnimationIsGrowing=false,currentTextSizeFactor=1.0}, CharState{whichChar=3,textAnimationIsGrowing=false,dotAnimationIsGrowing=true,currentTextSizeFactor=0.0}] password:123

注意列表中第一个CharState不仅字符值被改了,动画进度和标志位也被重置。
mTextChar列表中第一个和第三个对象值完全一样,怀疑它们是同一个对象。

方案

append方法中添加去重判断,CharState toString打印hashcode值。

public void append(char c) {
    int visibleChars = mTextChars.size();
    CharSequence textbefore = getTransformedText();
    mText = mText + c;//字串拼接
    int newLength = mText.length();
    CharState charState;
    if (newLength > visibleChars) {
		while(mTextChars.contains(charState)){//去重
		    android.util.Log.e("xiaohe","PasswordTextview append contains=== ");
		    charState = obtainCharState(c);
		}
		charState.reset();//从obtainCharState中移出来,确认不重复再重置状态赋值
		charState.whichChar = c;//从obtainCharState中移出来,确认不重复再重置状态赋值
		mTextChars.add(charState);
    }
...
}

修改后问题压测不复现

复测Log分析
//第一次点击
//mCharPool栈信息
01-06 11:54:16.592287  1907  1907 D xiaohe  : obtainCharState === mCharPool: [CharState{whichChar=0,HashCode=5aba0d}, CharState{whichChar=0,HashCode=8e2cc34}, CharState{whichChar=0,HashCode=b1ecc7e}, CharState{whichChar=1,HashCode=44fe8e}, CharState{whichChar=0,HashCode=3342ac4}, CharState{whichChar=0,HashCode=3342ac4}]
//从obtainCharState中pop出一个对象
01-06 11:54:16.592395  1907  1907 D xiaohe  : =====PasswordTextview append charState:CharState{whichChar=0,HashCode=3342ac4}
//打印列表mTextChars列表
01-06 11:54:16.592408  1907  1907 D xiaohe  : =====PasswordTextview append mTextChars:[]
//列表为空,中没有包含3342ac4对象
//将要添加到列表中的对象确定为3342ac4
01-06 11:54:16.592483  1907  1907 W xiaohe  : +++++PasswordTextview append charState:CharState{whichChar=1,HashCode=3342ac4}
//添加完之后的列表
01-06 11:54:16.592523  1907  1907 W xiaohe  : +++++PasswordTextview append mTextChars:[CharState{whichChar=1,HashCode=3342ac4}]
01-06 11:54:16.592538  1907  1907 W xiaohe  : PasswordTextview append display:[CharState{whichChar=1,HashCode=3342ac4}] password:1
//第二次点击
//mCharPool栈信息
01-06 11:54:16.791004  1907  1907 D xiaohe  : obtainCharState === mCharPool: [CharState{whichChar=0,HashCode=5aba0d}, CharState{whichChar=0,HashCode=8e2cc34}, CharState{whichChar=0,HashCode=b1ecc7e}, CharState{whichChar=1,HashCode=44fe8e}, CharState{whichChar=1,HashCode=3342ac4}]
//从obtainCharState中pop出一个对象
01-06 11:54:16.791055  1907  1907 D xiaohe  : =====PasswordTextview append charState:CharState{whichChar=1,HashCode=3342ac4}
//打印列表mTextChars列表
01-06 11:54:16.791067  1907  1907 D xiaohe  : =====PasswordTextview append mTextChars:[CharState{whichChar=1,HashCode=3342ac4}]
//新增检测机制,判断3342ac4已经存在列表中
01-06 11:54:16.791092  1907  1907 E xiaohe  : PasswordTextview append contains===
//mCharPool栈信息
01-06 11:54:16.791120  1907  1907 D xiaohe  : obtainCharState === mCharPool: [CharState{whichChar=0,HashCode=5aba0d}, CharState{whichChar=0,HashCode=8e2cc34}, CharState{whichChar=0,HashCode=b1ecc7e}, CharState{whichChar=1,HashCode=44fe8e}]
//再次从obtainCharState中pop出一个对象
//列表中不包含新的对象44fe8e
01-06 11:54:16.791169  1907  1907 W xiaohe  : +++++PasswordTextview append charState:CharState{whichChar=2,HashCode=44fe8e}
//添加完之后的列表
01-06 11:54:16.791181  1907  1907 W xiaohe  : +++++PasswordTextview append mTextChars:[CharState{whichChar=1,HashCode=3342ac4}, CharState{whichChar=2,HashCode=44fe8e}]
01-06 11:54:16.791196  1907  1907 W xiaohe  : PasswordTextview append display:[CharState{whichChar=1,HashCode=3342ac4}, CharState{whichChar=2,HashCode=44fe8e}] password:12

第一次点击,mCharPool栈里面pop出一个对象3342ac4,添加到append display列表中
第二次点击,mCharPool再次pop出一个对象3342ac4,试图添加到列表中,把对象改为2时。因为和第一个对象是同一个对象,对象的值和动画进度都被重置到初始状态,即显示22.
在这里添加判断,判断到列表中已经有该对象,不进行添加,再次pop出下一个元素44fe8e用来记录第二次的点击值。

为什么mCharPool里的值会重复

在mCharPool.push的地方添加堆栈打印log。
1、点击删除按钮,做移除动画,动画结束时

@Override
public void onAnimationEnd(Animator animation) {
    if (!mCancelled) {
        mTextChars.remove(CharState.this);//动画结束,从列表删除对象
        mCharPool.push(charState);//动画结束,把对象添加到栈
        reset();
        cancelAnimator(textTranslateAnimator);
        textTranslateAnimator = null;
    }
}
01-08 15:01:55.088606  1823  1823 D xiaohe  : reset startRemoveAnimation dotNeedsAnimation: true textNeedsAnimation: false widthNeedsAnimation: true ---- java.lang.Throwable
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at com.android.keyguard.PasswordTextView$CharState.startRemoveAnimation(PasswordTextView.java:677)
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at com.android.keyguard.PasswordTextView.deleteLastChar(PasswordTextView.java:361)
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at com.android.keyguard.KeyguardPinBasedInputViewController.lambda$onViewAttached$3(KeyguardPinBasedInputViewController.java:90)
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at com.android.keyguard.KeyguardPinBasedInputViewController.$r8$lambda$sVhaIBw-pegVYYoUq1EphloEbjc(Unknown Source:0)
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at com.android.keyguard.KeyguardPinBasedInputViewController$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at android.view.View.performClick(View.java:7455)
01-08 15:01:55.088606  1823  1823 D xiaohe  : 	at android.view.View.performClickInternal(View.java:7428)

2、KeyguardAbsKeyInputViewController.onPause,调用reset直接push了对象。

01-08 15:01:55.134947  1823  1823 D xiaohe  : reset animated: false announce: false length: 1
01-08 15:01:55.135042  1823  1823 D xiaohe  : reset mCharPool.push(charState): CharState{whichChar=1,HashCode=93c0038}
01-08 15:01:55.135716  1823  1823 I xiaohe  :  ---- java.lang.Throwable
01-08 15:01:55.135716  1823  1823 I xiaohe  : 	at com.android.keyguard.PasswordTextView.reset(PasswordTextView.java:452)
01-08 15:01:55.135716  1823  1823 I xiaohe  : 	at com.android.keyguard.KeyguardPinBasedInputView.resetPasswordText(KeyguardPinBasedInputView.java:148)
01-08 15:01:55.135716  1823  1823 I xiaohe  : 	at com.android.keyguard.KeyguardAbsKeyInputViewController.reset(KeyguardAbsKeyInputViewController.java:142)
01-08 15:01:55.135716  1823  1823 I xiaohe  : 	at com.android.keyguard.KeyguardAbsKeyInputViewController.onPause(KeyguardAbsKeyInputViewController.java:394)
01-08 15:01:55.135716  1823  1823 I xiaohe  : 	at com.android.keyguard.KeyguardPinViewController.onPause(KeyguardPinViewController.java:166)

2者同时操作了同一个对象,导致push重复。

尾注

由此分析,确实为列表中有重复对象导致此问题,去重方案已生效,问题修复。