http://www.cocoachina.com/ios/20160106/14889.html
檢查項目bug的時候偶然發現,做過限制(比如說字數、表情)的TextField、TextView,觸發限制條件後,會在使用undo功能時crash,之後發現微信也是一樣的。
有朋友問在哪裏崩了,不能復現,我舉幾個例子,其實有字數限制的輸入框應該都有問題。
我->個人信息->我的地址->新增地址
我->個人信息->名字
我->個人信息->個性簽名
隨便試了試qq、yy、簡書、喜馬拉雅的能輸入漢字的輸入框的字數限制,發現qq一般只提示不限制;yy禁用了undo;簡書沒做限制;做的最爛的是喜馬拉雅,做了限制,但是可以輕鬆突破,輸入任意長度的字符串。
DEMO:https://github.com/liulishuo/testUndo
思路
出現crash是因爲,爲了實現輸入的過濾效果,會監聽輸入框的UIControlEventEditingChanged事件,截取字符串,手動給輸入框的text屬性賦值。正常情況下輸入框執行setText:,默認不會註冊到自己的undoManager上,並且會清空undoManger的undo、redo棧,這樣並沒有問題,問題是在於監聽UIControlEventEditingChanged事件所執行的方法裏是先對輸入框的text做截取然後執行setText:。
看起來是截取的操作會入undo棧,之後的setText:方法並不會清空undo棧,導致做undo操作時,逆操作的是字符串截取的操作,操作的數據對不上,導致崩潰,這是我覺得比較合理的解釋。
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {6, 4} out of bounds; string length 6'
以此爲前提,我們有三個解決問題的方向:
-
禁用undo功能,繞過去,yy是這樣做的。
-
使用setText:,每次在過濾操作時先將setText:註冊到undoManager上,再進行setText:賦值操作。我試過不行。
還是一樣的錯誤-[NSBigMutableString substringWithRange:] range超限了,setText:的逆操作爲啥也是這個,我不清楚。
-
使用setText:,並確保和系統默認行爲一致,也就是用setText:賦值,並清空undo棧。
個人覺得這樣能達到目的,最方便。
實現
先說微信,微信的輸入框特點是:
1.漢字聯想的時的字符數也一樣有限制
2.文本長度滿了,輸入框就不能從任意位置插入任何字符(可能是爲了規避系統九宮格鍵盤輸入漢字的問題)
(不太好歸納,我的地址的收貨人輸入框貌似有兩套邏輯,一個是最大長度16個字,另外一個是最大長度50個字,我每次crash回來都會切換。。。,但是兩套邏輯都有各自的問題,有興趣的同學自己試一下,我們這裏只討論會crash的情況,也就是最大長度16個字的限制條件下的問題)
所以我一開始以爲,微信應該是這麼實現的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { //退格 if ([string isEqualToString:@ "" ]) { return YES; } //文本長度滿不允許編輯 防止系統九宮格鍵盤在此時傳入數字標號字符 if (textField.text.length >= kMaxLength) { return NO; } //非聯想狀態 if (!textField.markedTextRange) { NSString * tempString = [textField.text stringByReplacingCharactersInRange:range withString:string]; NSLog(@ "%@" ,tempString); if (tempString.length > kMaxLength) { textField.text = [tempString substringToIndex:kMaxLength]; return NO; } } return YES; } |
但是這樣寫不會因爲undo而crash,並且還有漢字聯想無限輸入的bug。
所以微信應該還用了這種方式
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//微信 [_tf addTarget:self action:@selector(textFieldTextDidChanged:) forControlEvents:UIControlEventEditingChanged]; - (void)textFieldTextDidChanged:(UITextField *)sender { NSString * tempString = sender.text; if (sender.markedTextRange == nil && tempString.length > kMaxLength) { sender.text = [tempString substringToIndex:kMaxLength]; } } |
這種方式除了undo會crash,沒有其他明顯的漏洞。
修復這個bug,只需要加一行代碼
1
2
3
4
5
6
7
8
9
10
|
- (void)textFieldTextDidChanged:(UITextField *)sender { NSString * tempString = sender.text; if (sender.markedTextRange == nil && tempString.length > kMaxLength) { sender.text = [tempString substringToIndex:kMaxLength]; [sender.undoManager removeAllActions]; } } |
末
just for fun 我們來猜一下其他人的實現
yy的實現(機智)
1
2
3
4
5
6
7
|
//yy - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { application.applicationSupportsShakeToEdit = NO; return YES; } |
喜馬拉雅的實現(漏洞最多)
1
2
3
4
5
6
7
8
9
10
11
12
|
//喜馬拉雅 - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { if (range.location >= kMaxLength) { return NO; } else { return YES; } } |
其實我覺得在用戶的輸入階段就屏蔽掉某些可能的輸入,真是一件吃力不討好的事情。