IOS过滤全局的Emoji表情输入

如摘要描述的上下文,现在需要解决Emoji输入导致的数据请求Error的问题。

问题原因:

在Unicode编码中,Emoji字符最小2个字节,小部分3个字节,更多4个字节,更丧心病狂的是有些输入法(比如某狗),存在11个字节的Emoji。而我们后端使用的数据库是MySql,默认使用Utf8编码,而默认的Utf8编码对字符的理解就是三个字节,所以当超过三个字节的Emoji入库时,就会报错执行失败(Incorrect string value '\xx\xx\xx\xx' for column 'field' at row 1),简单来说就是放不下,这问题其实修改MySql编码为Utf8mb4可以解决,但数据库改编码这种事情,是很敏感的,反正各种原因下,这问题得客户端解决。

需求分析:

我们的APP是一款汽车工具类APP,所提交的数据都是严谨且有意义的,而Emoji这种数据对我们来说可以说是可有可无。之前的做法是不做过滤不做限制(除格式要求区域外),现在既然遇到了问题,那索性的,将Emoji输入全部过滤掉。

方案分析:

需求已定,那现在的问题就有两个,一是如何全局过滤UITextField的输入(UITextView之类的我们没用),二是Emoji表情如何去过滤。这俩问题我们分开的讨论。

问题追究:

一、全局拦截UITextField输入

方案一:UITextFiled输入的信息最终都走网络接口发往服务器,基于这个,可以在网络层对所有Params做过滤,可保证到达服务器的数据不包含Emoji信息。但仅仅是解决了服务器报错问题,对客户端的体验很糟糕(明明可以输入Emoji,可数据拉取之后却丢失),另外,网络包是个公共的区域,它提供全局甚至多APP服务,改这里带来的测试成本条件不允许,所以方案一否定。

方案二:因为这款APP的UI是纯代码编写,我们可以扩展一个EmojiTextField,在内部代理系统默认的Delegate,对Emoji在输入时做屏蔽,这样能提供最好的输入体验,而且输入的信息和数据库信息一致。可这个方案需要替换全局的UITextField,虽然Xcode能很容易做到这点,但做法略显粗暴,做待选方案。

方案三:Runtime,我一直认为OC的强大至少一半归功于Runtime,它能在运行时修改函数表,而"Category"特性又提供了函数的扩展和覆盖,这让方案三的实现成为可能。

具体实现,导入全局的Category,这里叫UITextField+Emoji,覆盖+load函数(该函数在当前类读入内存时会收到消息),在该函数中通过Runtime替换初始化和Delegate相关函数,在初始化函数中,我们代理业务代码设置的Delegate,当用户有输入操作时,会触发我们代理的Delegate,在处理完Emoji校验之后,再路由给业务代码的Delegate。

我们项目采用.pch做全局h导入,在这里我们导入UITextField+Emoji的Gategory

/// 过滤Emoji字符
#import "UITextField+EmojiText.h"

在.m中,我们在+load函数内替换相关函数指针,让原有函数指向我们实现的IMP

void exchangeMethod(Class class, SEL oSEL, SEL nSEL)
{
    Method oMethod = class_getInstanceMethod(class, oSEL);
    Method nMethod = class_getInstanceMethod(class, nSEL);
    
    // 验证当前实例是否实现originalSEL,避免返回父类SEL
    BOOL ok = class_addMethod(class, oSEL, method_getImplementation(nMethod), method_getTypeEncoding(nMethod));
    if (ok) {
        class_replaceMethod(class, nSEL, method_getImplementation(oMethod), method_getTypeEncoding(oMethod));
    } else {
        method_exchangeImplementations(oMethod, nMethod);
    }
}

+ (void) load
{
    // setDelegate,拦截Delegate设置,默认走Emoji过滤
    exchangeMethod([self class], @selector(setDelegate:), @selector(emoji_setDelegate:));
    // getDelegate,返回业务代码设置的Delegate,确保set和get统一
    exchangeMethod([self class], @selector(delegate), @selector(emoji_delegate));
    // 几种初始化情况
    exchangeMethod([self class], @selector(init), @selector(emoji_init));
    exchangeMethod([self class], @selector(initWithFrame:), @selector(emoji_initWithFrame:));
    exchangeMethod([self class], @selector(initWithCoder:), @selector(emoji_initWithCoder:));
    // 释放内部持有资源
    exchangeMethod([self class], @selector(dealloc), @selector(emoji_dealloc));
}

相关替换的函数实现

- (id) emoji_init
{
    id ret = [self emoji_init];
    
    // 因为执行了函数指针替换,setDelegate会走emoji_setDelegate,这里调用setDelegate是为了确保没有设置delegate的业务代码同样过滤Emoji
    self.delegate = nil;
    
    return ret;
}

- (id) emoji_initWithFrame:(CGRect)frame
{
    id ret = [self emoji_initWithFrame:frame];
    
    self.delegate = nil;
    
    return ret;
}

- (id) emoji_initWithCoder:(NSCoder *)aDecoder
{
    id ret = [self emoji_initWithCoder:aDecoder];
    
    self.delegate = nil;
    
    return ret;
}

- (void) emoji_setDelegate:(id<UITextFieldDelegate>)delegate
{
    // 如果没有设置过delegate,需要设置内部代理的Delegate,否则让替换内部originalDelegate
    id<UITextFieldDelegate> del = [self emoji_delegate];
    if (!del) {
        EmojiDelegate *emojiDelegate = [[EmojiDelegate alloc] initWithTextField:self];
        emojiDelegate.originalDelegate = delegate;
        [self emoji_setDelegate:emojiDelegate];
    } else {
        EmojiDelegate *emojiDelegate = (EmojiDelegate *) del;
        emojiDelegate.originalDelegate = delegate;
    }
}

- (id<UITextFieldDelegate>) emoji_delegate
{
    return ((EmojiDelegate *)[self emoji_delegate]).originalDelegate;
}

- (void) emoji_dealloc
{
    // EmojiDelegate默认是retain的,需要手动释放一次资源
    [[self emoji_delegate] release];
    [self emoji_setDelegate:nil];
    [self emoji_dealloc];
}

EmojiDelegate的具体实现,很简单很单纯的一个代理

@interface EmojiDelegate : NSObject<UITextFieldDelegate>
@property(nonatomic, weak) UITextField *textField;
@property(nonatomic, weak) id<UITextFieldDelegate> originalDelegate;

@property(nonatomic, strong) NSString *prevText;    // 上次的输入结果

- (id) initWithTextField:(UITextField *)textField;
@end

@implementation EmojiDelegate

- (id) initWithTextField:(UITextField *)textField
{
    self = [super init];
    
    self.textField = textField;
    [textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
    
    return self;
}

- (void) dealloc
{
    [_textField removeTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
    self.originalDelegate = nil;
    self.prevText = nil;
    [super dealloc];
}

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
    if ([self.originalDelegate respondsToSelector:@selector(textFieldShouldBeginEditing:)]) {
        return [self.originalDelegate textFieldShouldBeginEditing:textField];
    }
    return YES;
}

- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    if ([self.originalDelegate respondsToSelector:@selector(textFieldDidBeginEditing:)]) {
        return [self.originalDelegate textFieldDidBeginEditing:textField];
    }
}

- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
    if ([self.originalDelegate respondsToSelector:@selector(textFieldShouldEndEditing:)]) {
        return [self.originalDelegate textFieldShouldEndEditing:textField];
    }
    return YES;
}

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    if ([self.originalDelegate respondsToSelector:@selector(textFieldDidEndEditing:)]) {
        return [self.originalDelegate textFieldDidEndEditing:textField];
    }
}

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (string.length == 0) {
        return YES;
    }
    
    /// 过滤emoji
    
    // 忽略系统默认的emoji键盘
    if ([[[textField textInputMode] primaryLanguage] isEqualToString:@"emoji"]) {
        return NO;
    }
    
    // 验证string的emoji字符
    if ([string containEmoji]) {
        return NO;
    }
    
    if ([self.originalDelegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
        return [self.originalDelegate textField:textField shouldChangeCharactersInRange:range replacementString:string];
    }
    return YES;
}

- (BOOL)textFieldShouldClear:(UITextField *)textField
{
    if ([self.originalDelegate respondsToSelector:@selector(textFieldShouldClear:)]) {
        return [self.originalDelegate textFieldShouldClear:textField];
    }
    return NO;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    if ([self.originalDelegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
        return [self.originalDelegate textFieldShouldReturn:textField];
    }
    return NO;
}

/**
 * 监听UITextField文本变动,规避中文输入法联想输入Emoji问题
 */
- (void) textFieldDidChange:(UITextField *)textField
{
    if ([textField markedTextRange] == nil) {
        NSString *text = textField.text;
        if ([text containEmoji]) {
            NSUInteger location = [textField selectedRange].location - 2;
            textField.text = _prevText;
            if (location > _prevText.length) {
                location = _prevText.length;
            }
            [textField setSelectedRange:NSMakeRange(location, 0)];
        } else {
            self.prevText = text;
        }
    }
}

这里重点在containEmoji函数,内部验证输入string是否包含Emoji元素。

至此,全局拦截问题解决,至于具体的Emoji过滤,接下来讨论。

 

二、字符串过滤Emoji

和大多数人一样,在解决这个问题时,百度谷歌bing一通搜索,找到了很多种解决方案,可实际效果都不尽人意,Emoji这问题比想象中要麻烦的多。

做之前做下简单扫盲,Emoji来源就不多说了,只要知道在某个版本的Unicode编码中加入了Emoji,并且不是放一块的,也就说在Unicode编码中,Emoji的地址没有规律可寻,那只能去硬匹配,可Emoji数量几百上千,这一个个去匹配实在太蠢了,咱得缩小匹配范围。

相信现在大家都用的UTF8编码,这是一种变长编码,提到变长,那肯定会有一个描述头,几个内容体,UTF8是一样的。

在一个字节中,如果第一个bit位是0,那么代表当前为单字节字符,0之后的7位bit为数据部分,代表在Unicode中的序号

对应的,如果第一位是1开头,代表是多字节字符,如果第二位是0,代表这个字节是多字节字符的数据字节,跟在头字节后面;如果前面有多个1,则几个1代表该字符有几个字节(包含当前字节),例如:

110xxxxx // 代表有两个字节,后面一定跟着一个10开头的数据字节
>>110xxxxx 10xxxxxx

1110xxxx // 代表有三个字节,后面跟着两个10开头的数据字节
>>1110xxxx 10xxxxxx 10xxxxxx

推理可知,Utf8中一个字符最长7个字节,其中数据位6个字节,其中Emoji在Unicode中分布在2、3、4、4+长度的地址中,其中长度为2的Emoji大部分是文字字符,这些我们可以放行,4、4+的Emoji可全部过滤,而我们可见文字基本都分部在3字节地址中,这里重点需要过滤3字节的Emoji(3字节的Emoji已经可以入库了,但为了统一体验,还是需要过滤掉),幸运的是3字节的Emoji不是很多,硬匹配也算说得过去。

根据从Unicode官网找到的资料,匹配三字节Unicode

- (BOOL) emojiInUnicode:(short)code
{
    if (code == 0x0023
        || code == 0x002A
        || (code >= 0x0030 && code <= 0x0039)
        || code == 0x00A9
        || code == 0x00AE
        || code == 0x203C
        || code == 0x2049
        || code == 0x2122
        || code == 0x2139
        || (code >= 0x2194 && code <= 0x2199)
        || code == 0x21A9 || code == 0x21AA
        || code == 0x231A || code == 0x231B
        || code == 0x2328
        || code == 0x23CF
        || (code >= 0x23E9 && code <= 0x23F3)
        || (code >= 0x23F8 && code <= 0x23FA)
        || code == 0x24C2
        || code == 0x25AA || code == 0x25AB
        || code == 0x25B6
        || code == 0x25C0
        || (code >= 0x25FB && code <= 0x25FE)
        || (code >= 0x2600 && code <= 0x2604)
        || code == 0x260E
        || code == 0x2611
        || code == 0x2614 || code == 0x2615
        || code == 0x2618
        || code == 0x261D
        || code == 0x2620
        || code == 0x2622 || code == 0x2623
        || code == 0x2626
        || code == 0x262A
        || code == 0x262E || code == 0x262F
        || (code >= 0x2638 && code <= 0x263A)
        || (code >= 0x2648 && code <= 0x2653)
        || code == 0x2660
        || code == 0x2663
        || code == 0x2665 || code == 0x2666
        || code == 0x2668
        || code == 0x267B
        || code == 0x267F
        || (code >= 0x2692 && code <= 0x2694)
        || code == 0x2696 || code == 0x2697
        || code == 0x2699
        || code == 0x269B || code == 0x269C
        || code == 0x26A0 || code == 0x26A1
        || code == 0x26AA || code == 0x26AB
        || code == 0x26B0 || code == 0x26B1
        || code == 0x26BD || code == 0x26BE
        || code == 0x26C4 || code == 0x26C5
        || code == 0x26C8
        || code == 0x26CE
        || code == 0x26CF
        || code == 0x26D1
        || code == 0x26D3 || code == 0x26D4
        || code == 0x26E9 || code == 0x26EA
        || (code >= 0x26F0 && code <= 0x26F5)
        || (code >= 0x26F7 && code <= 0x26FA)
        || code == 0x26FD
        || code == 0x2702
        || code == 0x2705
        || (code >= 0x2708 && code <= 0x270D)
        || code == 0x270F
        || code == 0x2712
        || code == 0x2714
        || code == 0x2716
        || code == 0x271D
        || code == 0x2721
        || code == 0x2728
        || code == 0x2733 || code == 0x2734
        || code == 0x2744
        || code == 0x2747
        || code == 0x274C
        || code == 0x274E
        || (code >= 0x2753 && code <= 0x2755)
        || code == 0x2757
        || code == 0x2763 || code == 0x2764
        || (code >= 0x2795 && code <= 0x2797)
        || code == 0x27A1
        || code == 0x27B0
        || code == 0x27BF
        || code == 0x2934 || code == 0x2935
        || (code >= 0x2B05 && code <= 0x2B07)
        || code == 0x2B1B || code == 0x2B1C
        || code == 0x2B50
        || code == 0x2B55
        || code == 0x3030
        || code == 0x303D
        || code == 0x3297
        || code == 0x3299
        // 第二段
        || code == 0x23F0) {
        return YES;
    }
    return NO;
}

另外还有很古老的一套Emoji,采用Unicode私有区域,现在基本没用了,不过还是过滤下

/**
 * 一种非官方的, 采用私有Unicode 区域
 * e0 - e5  01 - 59
 */
- (BOOL) emojiInSoftBankUnicode:(short)code
{
    return ((code >> 8) >= 0xE0 && (code >> 8) <= 0xE5 && (Byte)(code & 0xFF) < 0x60);
}

另外就是对输入string的过滤,需要过滤掉字节长度为非3的字符,然后校验3字节的unicode编码

- (BOOL) containEmoji
{
    NSUInteger len = [self lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
    if (len < 3) {  // 大于2个字符需要验证Emoji(有些Emoji仅三个字符)
        return NO;
    }
    
    // 仅考虑字节长度为3的字符,大于此范围的全部做Emoji处理
    NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
    
    Byte *bts = (Byte *)[data bytes];
    Byte bt;
    short v;
    for (NSUInteger i = 0; i < len; i++) {
        bt = bts[i];
        
        if ((bt | 0x7F) == 0x7F) {  // 0xxxxxxx  ASIIC编码
            continue;
        }
        if ((bt | 0x1F) == 0xDF) {  // 110xxxxx  两个字节的字符
            i += 1;
            continue;
        }
        if ((bt | 0x0F) == 0xEF) {  // 1110xxxx  三个字节的字符(重点过滤项目)
            // 计算Unicode下标
            v = bt & 0x0F;
            v = v << 6;
            v |= bts[i + 1] & 0x3F;
            v = v << 6;
            v |= bts[i + 2] & 0x3F;
            
//            NSLog(@"%02X%02X", (Byte)(v >> 8), (Byte)(v & 0xFF));

            if ([self emojiInSoftBankUnicode:v] || [self emojiInUnicode:v]) {
                return YES;
            }
            
            i += 2;
            continue;
        }
        if ((bt | 0x3F) == 0xBF) {  // 10xxxxxx  10开头,为数据字节,直接过滤
            continue;
        }
        
        return YES;                 // 不是以上情况的字符全部超过三个字节,做Emoji处理
    }
    
    return NO;
}

然后将相关函数封装为NSString (Emoji)

完工。

 

相关参考资料:

http://tech.glowing.com/cn/method-swizzling-aop/

http://www.unicode.org/Public/emoji/3.0//emoji-data.txt

 

转载于:https://my.oschina.net/5zx1Vm/blog/700546

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值