iOS-NSUndoManager與怎樣弄崩微信

http://www.cocoachina.com/ios/20160106/14889.html

檢查項目bug的時候偶然發現,做過限制(比如說字數、表情)的TextField、TextView,觸發限制條件後,會在使用undo功能時crash,之後發現微信也是一樣的。

有朋友問在哪裏崩了,不能復現,我舉幾個例子,其實有字數限制的輸入框應該都有問題。

15.png

我->個人信息->我的地址->新增地址

16.png

我->個人信息->名字

17.png

我->個人信息->個性簽名

隨便試了試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;
    }
}

其實我覺得在用戶的輸入階段就屏蔽掉某些可能的輸入,真是一件吃力不討好的事情。


發佈了47 篇原創文章 · 獲贊 11 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章