正文
在适配iOS13时,遇到一个挺棘手的问题:UITextField输入中文时,三指撤销竟然直接crash了。如果你也在做iOS13适配,很可能碰上这个坑。

Bugly报错
NSInternalInconsistencyException setGroupIdentifier:: _NSUndoStack 0x1206532f0 is in invalid state, calling setGroupIdentifier with no begin group mark
堆栈信息
从堆栈信息能很清楚地看到,crash发生在撤销操作链中,核心是_NSUndoStack的状态异常。
CoreFoundation ___exceptionPreprocess + 220 libobjc.A.dylib objc_exception_throw + 56 Foundation -[_NSUndoStack groupIdentifier] Foundation -[NSUndoManager undoNestedGroup] + 240 UIKitCore -[UIUndoGestureInteraction undo:] + 72 UIKitCore -[UIKBUndoInteractionHUD performDelegateUndoAndUpdateHUDIfNeeded] + 96 UIKitCore -[UIKBUndoInteractionHUD controlActionUpInside:] + 152 UIKitCore -[UIApplication sendAction:to:from:forEvent:] + 96 xxxxx -[UIApplication(MemoryLeak) swizzled_sendAction:to:from:forEvent:] + 288 UIKitCore -[UIControl sendAction:to:from:forEvent:] + 240 UIKitCore -[UIControl _sendActionsForEvents:withEvent:] + 408 UIKitCore -[UIControl touchesEnded:withEvent:] + 520 UIKitCore -[UIWindow _sendTouchesForEvent:] + 2324 UIKitCore -[UIWindow sendEvent:] + 3352 UIKitCore -[UIApplication sendEvent:] + 336 UIKitCore ___dispatchPreprocessedEventFromEventQueue + 5880 UIKitCore ___handleEventQueueInternal + 4924 UIKitCore ___handleHIDEventFetcherDrain + 108 CoreFoundation ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24 CoreFoundation ___CFRunLoopDoSource0 + 80 CoreFoundation ___CFRunLoopDoSources0 + 180 CoreFoundation ___CFRunLoopRun + 1080 CoreFoundation CFRunLoopRunSpecific + 464 GraphicsServices GSEventRunModal + 104 UIKitCore UIApplicationMain + 1936 xxxxx main + 148 libdyld.dylib _start + 4
问题定位
一时没什么头绪,最老实的办法就是一段一段注释代码。最终,矛头指向了下面这两个方法:
[self addTarget:observer
action:@selector(textChange:)
forControlEvents:UIControlEventEditingChanged];
- (void)textChange:(UITextField *)textField {
... ...
UITextRange *selectedRange = [textField markedTextRange];
if (!selectedRange || !selectedRange.start) {
if (destText.length > maxLength) {
textField.text = [destText substringToIndex:maxLength];
}
}
}
这段代码原本是用来限制文案长度的。问题在于,三指撤销操作会触发UIControlEventEditingChanged事件,从而执行textChange:。但此时即便输入法还存在高亮的markedText,markedTextRange也返回nil。紧接着,代码就会把textField.text给截断了。这一修改,直接破坏了撤销栈的内部状态,再继续执行撤销操作,crash就成了必然。
解决方案
既然问题是同步修改文本导致撤销栈混乱,那把截断操作延迟到下一个runloop执行就行了。方法很简单,将文案判长和截取的逻辑用dispatch_async扔到主队列:
- (void)textChange:(UITextField *)textField {
dispatch_async(dispatch_get_main_queue(), ^{
... ...
});
}
这样一来,截断操作就不会干扰当前正在进行的撤销流程了。
数字截断后 crash
中文输入的问题解决了,但数字键盘的场景还有坑。当UITextField限制了长度,超过最大长度后继续输入,再进行撤销操作同样会crash。而且上面的异步方案在这个场景下不奏效。目前比较稳妥的做法,是在UITextField的袋里回调中直接拦截输入:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
/// 输入数字后截取字符串仍旧可以触发撤销操作导致crash, 在这里拦截一下
if (textField.keyboardType == UIKeyboardTypeNumberPad
&& range.location >= textField.tt_maxLength) {
return NO;
}
return YES;
}
简单粗暴,在超出长度后直接拒绝输入,从源头避免crash。
