背景描述
客户反馈售后问题,设置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重复。
尾注
由此分析,确实为列表中有重复对象导致此问题,去重方案已生效,问题修复。