当前位置: 首页 > news >正文

IOS过滤全局的Emoji表情输入

为什么80%的码农都做不了架构师?>>>   hot3.png

如摘要描述的上下文,现在需要解决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

相关文章:

  • HTML5 网络拓扑图性能优化
  • [备忘]如何接收向shell脚本传入的参数
  • qt QClipBoard
  • redis window版本设置密码,但是无效的解决方案
  • Swift基础--Swift中的异常处理
  • 【iCore3 双核心板_ uC/OS-III】例程十一:任务消息队列
  • id 选择器
  • MYSQL 专家 ----zhaiwx_yinfeng
  • iOS开发-面试总结(六)笔试(二)
  • HoloToolkit项目源码剖析 - Spatial Mapping功能实现
  • html form post/get
  • 阿里云服务器Linux CentOS安装配置(四)yum安装tomcat
  • 3.saltstack模块介绍
  • Dedecms去掉URL中a目录的方法
  • 限制Java线程池运行线程以及等待线程数量的策略
  • docker容器内的网络抓包
  • flask接收请求并推入栈
  • Java 多线程编程之:notify 和 wait 用法
  • js
  • JS基础篇--通过JS生成由字母与数字组合的随机字符串
  • JWT究竟是什么呢?
  • Quartz初级教程
  • Redis的resp协议
  • unity如何实现一个固定宽度的orthagraphic相机
  • 基于MaxCompute打造轻盈的人人车移动端数据平台
  • 利用jquery编写加法运算验证码
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 使用Tinker来调试Laravel应用程序的数据以及使用Tinker一些总结
  • 学习笔记TF060:图像语音结合,看图说话
  • 原生js练习题---第五课
  • const的用法,特别是用在函数前面与后面的区别
  • JavaScript 新语法详解:Class 的私有属性与私有方法 ...
  • Java数据解析之JSON
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • ​DB-Engines 12月数据库排名: PostgreSQL有望获得「2020年度数据库」荣誉?
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (1综述)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练
  • (Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (九)信息融合方式简介
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (一)基于IDEA的JAVA基础10
  • (转)iOS字体
  • .NET Core 版本不支持的问题
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .NET MVC第五章、模型绑定获取表单数据
  • .NET WebClient 类下载部分文件会错误?可能是解压缩的锅
  • .NET 材料检测系统崩溃分析
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .NET大文件上传知识整理
  • .NET的数据绑定
  • .Net调用Java编写的WebServices返回值为Null的解决方法(SoapUI工具测试有返回值)