IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> ios 弹幕过滤敏感词方案对比和性能测试 -> 正文阅读

[移动开发]ios 弹幕过滤敏感词方案对比和性能测试

在看视频的过程中, 很多用户会发弹幕, 当前用户可以设置过滤敏感词和敏感用户,? 设置后, 命中敏感词和敏感用户的弹幕就不会显示.?

  • 敏感词和敏感用户的设置上限为各100.
  • 由客户端进行过滤,
  • 不区分大小写, 比如用户设置了"abc",? 其他用户发送了"ABC"或者"Abc", 都不显示.

过滤敏感用户

服务器对发送弹幕的用户ID做了16位的md5, 比如用户ID为12345, 经过16位MD5加密后为EA8A706C4C34A168, 客户端使用弹幕发送者的ID和数组(最多100个)中的敏感用户ID进行匹配,如果匹配到了就不展示该弹幕.

一开始的做法, 服务器返回了敏感用户的数组, 客户端使用数组的-?containsObject进行处理, 功能是可以完成, 但是由于containsObject 内部实现是做了一次O(N)的遍历, 假设有1W个敏感用户, 每条弹幕都需要循环1W次, 效率很差, 我们采用了生成一个NSSet, 使用Set的containsObject 进行判断, 这样时间复杂度就降到了O(1).

过滤敏感词

由于需要忽略弹幕中的大小写, 直接使用[NSString containsString:@""] 是不行的,? 经过一顿搜索, 使用谓词?可以忽略敏感词里的大小写.? 假设 @"abc" 为敏感词

NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS [cd] %@",@"abc"];
BOOL result1 = [pred evaluateWithObject:@"Abc"];
BOOL result2 = [pred evaluateWithObject:@"ABCD"];
BOOL result3 = [pred evaluateWithObject:@"ABC"];
BOOL result4 = [pred evaluateWithObject:@"AC"];
// 打印结果  1   1   1  0
NSLog(@"%d  %d  %d  %d",result1, result2, result3, result4);

可以达成效果,? 很快写下了这样的代码. 自测通过, 继续做其他功能 ...

// self.keyWordArray 为用户设置的敏感词构成的数组
// self.danMu 为其他用户发送的弹幕, 判断此条弹幕是否合法
for (NSString *keyWord in self.keyWordArray) {
    NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS [cd] %@",keyWord];
    BOOL result = [pred evaluateWithObject:self.danMu];
    if (result) {
        NSLog(@"方案1 -- %@",keyWord);
    }
}

提测之后, 测试环境一切正常, 但是到了正式环境上, 发现弹幕存在卡顿,? 由于测试环境的弹幕普遍不多(基本不超过100条),? 而正式环境上的弹幕很多都是几千条,在一个3W条弹幕的视频进行测试, 可以感受到明显的卡顿.

抓紧时间进行优化

  1. 优化判断时机,?
  2. 缓存NSPredicate对象

优化判断时机,

之前的做法是在开始播放后进行全量的数据判断, 比如说总计有1W条弹幕, 开始播放后, 立即逐个判断弹幕是否合法, 然后存到新数组中, 从新数组中查找弹幕进行展示, 这样的做法就是会导致刚开始播放CPU很高, 而且所有数据处理完成后才能展示弹幕, 在加过滤功能之前, 只要开始播放就可以展示弹幕, 而现在要等2-3S才能开始展示弹幕.

修改判断的时机, 在每次取出弹幕的时候进行判断, 比如这1s取出100条弹幕, 那只判断这100条弹幕是否合法, 不判断全量数据, 虽然判断的总数没有变化, 但是每次判断量很小, 弹幕可以很快出现. 把一个CPU占用的高峰, 分配到了播放的过程中, 平滑CPU的波动.

缓存NSPredicate对象

使用xcode -> instrument查看CPU占用, 发现生成谓词对象和使用谓词判断占用了很多cpu时间, 由于谓词对象是和服务器返回的敏感词绑定的, 而且在播放的过程中没有变化, 可以使用数组来缓存谓词对象, 不需要在每次判断生成一次.

假设总计有1W条弹幕+100个敏感词?进行判断, 那么原始的写法会生成 100W 个临时谓词变量
如果采用缓存谓词对象后, 只需在服务器返回数据后生成100次即可, 后续都是取出谓词进行判断, 可以节省大约100W次谓词生成占用的CPU消耗.
弹幕数量越多, 缓存的优势越明显.

for (NSString *keyWord in self.keyWordArray) {
    // 生成谓词对象很费时间
    NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS [cd] %@",keyWord];
    BOOL result = [pred evaluateWithObject:self.danMu];
}

优化后写法 🔽🔽🔽

for (NSString *keyWord in self.keyWordArray) {
    // 数组缓存谓词对象, 取出谓词进行判断
    NSPredicate *pred = [self.predArray objectAtIndex:i];
    BOOL result = [pred evaluateWithObject:self.danMu];
}

由于多用了缓存, 自然关心一下内存的增长, 如果缓存的谓词对象太大, 导致内存上升几十M,甚至上百M, 那么这个方案,肯定不会通过.

新建一个项目进行验证, 无关因素少, 验证后发现 100个谓词的大小可以忽略不计,

  • 原始内存占用 ?106M
  • 缓存数据后占用 ?319M,
  • 缓存谓词占用增量为213M, 总计缓存100W个谓词对象,平均1W个谓词对象占用2.13M,
  • 单个谓词占用约0.2K,实际项目中100个内存占用约20K,可忽略不计

经过了这2个优化, 播放的卡顿已经没有了,按时上线.


以为这就完事了, NO,NO,NO, 虽然经过了优化, 但是谓词判断是否包含占用还是有点大, 就是这一行. 这个是每条弹幕都会调用的, 再找找有没有其他方案进行优化.

BOOL result = [pred evaluateWithObject:self.danMu];

在xcode中搜索是不区分大小写的, 而且搜索很快, 比如搜索 ABC, 是可以搜出来 abc, Abc, ABc, 那么系统应该提供出了类似的api,? 在NSString下搜索contain, 找到了这2个API,?

  • - (BOOL)localizedCaseInsensitiveContainsString:(NSString *)str;
    返回一个布尔值,通过执行不区分大小写、区分区域设置的搜索,指示该字符串是否包含给定字符串。

  • - (BOOL)localizedStandardContainsString:(NSString *)str;
    返回一个布尔值,该值指示字符串是否包含给定字符串,方法是执行不区分大小写和变音符号的区域设置搜索。

通过阅读官方的注释文档, 2个方法很接近, 区别在于是否区分变音符号, 对中文和英文来说应该没有区别. 这2个api最终都会调用此方法.?

- (NSRange)rangeOfString:(NSString *)searchString options:(NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToSearch locale:(nullable NSLocale *)locale

其中有2个参数着重说下?NSStringCompareOptions 和 NSLocale,?

typedef NS_OPTIONS(NSUInteger, NSStringCompareOptions) {
    NSCaseInsensitiveSearch = 1,
    NSLiteralSearch = 2,		/* Exact character-by-character equivalence */
    NSBackwardsSearch = 4,		/* Search from end of source string */
    NSAnchoredSearch = 8,		/* Search is limited to start (or end, if NSBackwardsSearch) of source string */
    NSNumericSearch = 64,		/* Added in 10.2; Numbers within strings are compared using numeric value, that is, Foo2.txt < Foo7.txt < Foo25.txt; only applies to compare methods, not find */
    NSDiacriticInsensitiveSearch API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 128, /* If specified, ignores diacritics (o-umlaut == o) */
    NSWidthInsensitiveSearch API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 256, /* If specified, ignores width differences ('a' == UFF41) */
    NSForcedOrderingSearch API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 512, /* If specified, comparisons are forced to return either NSOrderedAscending or NSOrderedDescending if the strings are equivalent but not strictly equal, for stability when sorting (e.g. "aaa" > "AAA" with NSCaseInsensitiveSearch specified) */
    NSRegularExpressionSearch API_AVAILABLE(macos(10.7), ios(3.2), watchos(2.0), tvos(9.0)) = 1024    /* Applies to rangeOfString:..., stringByReplacingOccurrencesOfString:..., and replaceOccurrencesOfString:... methods only; the search string is treated as an ICU-compatible regular expression; if set, no other options can apply except NSCaseInsensitiveSearch and NSAnchoredSearch */
};
  • NSCaseInsensitiveSearch = 1,//不区分大小写的搜索
  • NSLiteralSearch = 2, ? ? ? ?/* 精确的逐个字符串等价, - isEqualToString, Exact character-by-character equivalence */
  • NSBackwardsSearch = 4, ? ? ?/*从源字符串的末尾搜索、 Search from end of source string */
  • NSAnchoredSearch = 8, ? ? ? /*搜索仅限于开始(或结束,如果是从末尾开始的搜索)源字符串 Search is limited to start (or end, if NSBackwardsSearch) of source string */
  • NSNumericSearch = 64, ? ? ? /*。用字符串中的数字的值进行比较, Added in 10.2; Numbers within strings are compared using numeric value, that is, Foo2.txt < Foo7.txt < Foo25.txt; only applies to compare methods, not find */
  • NSDiacriticInsensitiveSearch = 128, /*搜索忽略变音符号。 If specified, ignores diacritics (o-umlaut == o) */
  • NSWidthInsensitiveSearch = 256 /* 搜索忽略具有全宽和半宽形式的字符的宽度差异,例如在东亚字符串集。If specified, ignores width differences ('a' == UFF41) */
  • NSForcedOrderingSearch = 512 /* 如果字符串是等效的但不是严格相等的,比较会被强制返回same,例如 "aaa"和"AAA" 会返回same。 */
  • NSRegularExpressionSearch = 1024?/* 只在rangeOfString:...、stringByReplacingOccurrencesOfString:...和replaceOccurrencesOfString:...方法中适用。搜索字符串被视为与ICU兼容的正则表达式。如果设置了这个选项,那么其余选项除了NSCaseInsensitiveSearch和NSAnchoredSearch,别的都不能使用 */

看来这2个的api差别就在于有没有设置NSDiacriticInsensitiveSearch ,? 同时还发现支持忽略标点符号,设置NSWidthInsensitiveSearch, 就可以忽略中文英文标点.

至于NSLocale, 就参考这篇文章??NSLocale的重要性和用法简介 - 简书

到此, 我们已经有4个方案了, 对比一下4个方案的性能,?

  1. 使用谓词, 不缓存谓词, 每次使用临时变量
  2. 使用谓词, 缓存谓词, 把谓词对象缓存到集合中
  3. 使用NSString的方法, 使用支持变音的版本, ios9之后可用
  4. 使用NSString的方法, 使用不支持变音的版本, ios 8之后可用

总体来看,

  • 使用String的效率比使用谓词要高效很多,
    即使缓存谓词, 使用谓词判断的耗时还是使用String判断的2倍以上, 所以, 可以考虑使用谓词方案替换成使用string的方案. 而2个string方案效率差别不大.
  • 使用谓词的好处也是有的, 就是比较灵活, 可以自由组合判断条件, 这点是String做不到的.
  • 使用谓词缓存, 可以提升3倍左右的效率.弹幕数量越多, 缓存的优势越明显.
  • 2个string版本中, 变音版本效率略高, 可能系统在ios9之后偷偷优化了实现, 推荐使用
  • 使用forin遍历效率比block遍历的效率略高一点点, 但是差别不大. 实际开发中基本无感觉

?最后, 附上压力测试的代码:?

#import "ViewController.h"

@interface ViewController ()
/// 敏感词数组
@property (nonatomic, strong) NSArray *keyWordArray;
/// 用户发送的文案
@property (nonatomic, copy) NSString *danMu;
// 遍历使用的方式
@property (nonatomic, assign) NSInteger type;

// 缓存谓词对象
@property (nonatomic, strong) NSArray <NSPredicate *>*predArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 模拟数据,压力测试, 假设有100W条敏感词
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:10000];
    for (NSInteger i = 0; i<10000 * 100; i++) {
        [array addObject:NSUUID.UUID.UUIDString];
    }
    [array addObject:@"敏感词1a"];
    self.keyWordArray = [array copy];
    self.danMu = @"来了,敏感词1A";
    self.type = 1;

    // 提前处理好谓词数组
    NSMutableArray *predArray = [NSMutableArray arrayWithCapacity:self.keyWordArray.count];
    for (NSString *keyWord in self.keyWordArray) {
        NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS [cd] %@",keyWord];
        [predArray addObject:pred];
    }
    self.predArray = [predArray copy];

#pragma mark - 开始测试
    // 100W条数据  3.83S, 3.47S  4.92S forin遍历
    // 100W条数据  4.62S, 3.64S  4.01S block遍历, 使用block遍历还要略慢一点
    // 缓存谓词结果后,100W条数据 0.67S  0.83S  0.71S, 比下面的2种方案性能还是差, 但是在可接受范围内

    CFTimeInterval start = CACurrentMediaTime();
    [self test1];
    CFTimeInterval end = CACurrentMediaTime();
    NSLog(@"方案1  %@",@(end-start));


    // 提前处理好谓词缓存
    // 100W条数据  1.37S  1.18S  1.24S  forin遍历
    // 100W条数据  1.24S  1.26S  1.25S  block遍历
    // 原始内存占用  106M
    // 缓存数据后占用  319M,
    // 缓存占用增量为213M,缓存100W个对象,平均1W个对象占用2.13M,单个谓词占用0.2K,实际项目中100个内存占用约20K, 可忽略不计
    start = CACurrentMediaTime();
    [self test11];
    end = CACurrentMediaTime();
    NSLog(@"方案11  %@",@(end-start));


    // 100W条数据  0.54S  0.37S  0.37S  forin遍历
    // 100W条数据  0.52S  0.43S  0.46S block遍历
    start = CACurrentMediaTime();
    [self test2];
    end = CACurrentMediaTime();
    NSLog(@"方案2  %@",@(end-start));

    // 100W条数据  0.58S  0.39S  0.49S  forin遍历
    // 100W条数据  0.65S 0.44S  0.47S block遍历
    start = CACurrentMediaTime();
    [self test3];
    end = CACurrentMediaTime();
    NSLog(@"方案3  %@",@(end-start));
#pragma mark 结束测试
}


- (void)test1 {
    // 方案1, 使用谓词,可以比较,不区分大小写
    if (self.type == 0) {
        for (NSString *keyWord in self.keyWordArray) {
            // 生成谓词对象很费时间,可以用数组缓存谓词对象
            NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS [cd] %@",keyWord];
            BOOL result = [pred evaluateWithObject:self.danMu];
            if (result) {
                NSLog(@"方案1 -- %@",keyWord);
            }
        }
    } else if (self.type == 1) {
        [self.keyWordArray enumerateObjectsUsingBlock:^(NSString * _Nonnull keyWord, NSUInteger idx, BOOL * _Nonnull stop) {
            NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS [cd] %@",keyWord];
            BOOL result = [pred evaluateWithObject:self.danMu];
            if (result) {
                NSLog(@"方案1 -- %@",keyWord);
            }
        }];
    }
}

- (void)test11 {
    // 方案11, 使用谓词,缓存谓词结果
    if (self.type == 0) {
        NSInteger i = 0;
        for (NSString *keyWord in self.keyWordArray) {
            // 生成谓词对象很费时间,可以用数组缓存谓词对象
            NSPredicate *pred = [self.predArray objectAtIndex:i];
            BOOL result = [pred evaluateWithObject:self.danMu];
            if (result) {
                NSLog(@"方案11 -- %@",keyWord);
            }
            i++;
        }
    } else if (self.type == 1) {
        [self.keyWordArray enumerateObjectsUsingBlock:^(NSString * _Nonnull keyWord, NSUInteger idx, BOOL * _Nonnull stop) {
            NSPredicate *pred = [self.predArray objectAtIndex:idx];
            BOOL result = [pred evaluateWithObject:self.danMu];
            if (result) {
                NSLog(@"方案11 -- %@",keyWord);
            }
        }];
    }
}

// 支持变音版本
- (void)test2 {
    if (self.type == 0) {
        for (NSString *keyWord in self.keyWordArray) {
            BOOL result = [self.danMu localizedStandardContainsString:keyWord];
            if (result) {
                NSLog(@"方案2 -- %@",keyWord);
            }
        }
    } else if (self.type == 1) {
        [self.keyWordArray enumerateObjectsUsingBlock:^(NSString * _Nonnull keyWord, NSUInteger idx, BOOL * _Nonnull stop) {
            BOOL result = [self.danMu localizedStandardContainsString:keyWord];
            if (result) {
                NSLog(@"方案2 -- %@",keyWord);
            }
        }];
    }

}

// 不支持变音版本
- (void)test3 {

    if (self.type == 0) {
        for (NSString *keyWord in self.keyWordArray) {
            BOOL result = [self.danMu localizedCaseInsensitiveContainsString:keyWord];
            if (result) {
                NSLog(@"方案3 -- %@",keyWord);
            }
        }
    } else if (self.type == 1) {
        [self.keyWordArray enumerateObjectsUsingBlock:^(NSString * _Nonnull keyWord, NSUInteger idx, BOOL * _Nonnull stop) {
            BOOL result = [self.danMu localizedCaseInsensitiveContainsString:keyWord];
            if (result) {
                NSLog(@"方案3 -- %@",keyWord);
            }
        }];
    }
}


// 忽略大小写进行比较是否相等,
- (void)test10 {
    NSComparisonResult result = [@"abc" caseInsensitiveCompare:@"ABc"];
//    a < b   , NSOrderedAscending. -1
//    a == b  , NSOrderedSame. 0
//    a > b   , NSOrderedDescending. 1
    NSLog(@"caseInsensitiveCompare -- %zd",result);
}


@end

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-26 11:52:02  更:2022-04-26 11:54:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 23:47:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码