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:@""] 是不行的,? 經過一頓搜索, 發現可以使用謂詞?可以忽略敏感詞里的大小寫.?
謂詞參考文章:
iOS謂詞 - 簡書
iOS-謂詞的使用詳解 - 簡書
IOS中謂詞的使用 - 簡書
使用謂詞檢索時對字符串比較運算符需要要不區分大小寫和重音符號,就要在這些字符串運算符后使用[c],[d]選項。其中[c]表示不區分大小寫,[d]表示不區分重音符號,[cd]表示即忽略大小寫又忽略重音符。需要將其寫在字符串比較運算符之后,比如:name LIKE [cd] 'string'
假設 @"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條彈幕的視頻進行測試, 可以感受到明顯的卡頓.
抓緊時間進行優化
優化判斷時機,
之前的做法是在開始播放后進行全量的數據判斷, 比如說總計有1W條彈幕, 開始播放后, 立即逐個判斷彈幕是否合法, 然后存到新數組中, 從新數組中查找彈幕進行展示, 這樣的做法就是會導致剛開始播放CPU很高, 而且所有數據處理完成后才能展示彈幕, 在加過濾功能之前, 只要開始播放就可以展示彈幕, 而現在要等2-3S才能開始展示彈幕.
修改判斷的時機, 在每次取出彈幕的時候進行判斷, 比如這1s取出100條彈幕, 那只判斷這100條彈幕是否合法, 不判斷全量數據, 雖然判斷的總數沒有變化, 但是每次判斷量很小, 彈幕可以很快出現. 把一個CPU占用的高峰, 分配到了播放的過程中, 平滑CPU的波動.
緩存NSPredicate對象
使用xcode -> instrument查看CPU占用, 發現生成謂詞對象和使用謂詞判斷占用了很多cpu時間, 由于謂詞對象是和服務器返回的敏感詞綁定的, 而且在播放的過程中沒有變化, 可以使用數組來緩存謂詞對象, 不需要在每次判斷生成一次.
假設總計有1W條彈幕+100個敏感詞?進行判斷, 那么原始的寫法會生成 100W 個臨時謂詞變量
如果采用緩存謂詞對象后, 只需在服務器返回數據后生成100次即可, 后續都是取出謂詞進行判斷, 可以節省大約100W次謂詞生成占用的CPU消耗.
彈幕數量越多, 緩存的優勢越明顯.
由于多用了緩存, 自然關心一下內存的增長, 如果緩存的謂詞對象太大, 導致內存上升幾十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個方案的性能,?
總體來看,
- 使用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. 1NSLog(@"caseInsensitiveCompare -- %zd",result); }@end總結
以上是生活随笔為你收集整理的ios 弹幕过滤敏感词方案对比和性能测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt编写可视化大屏电子看板系统4-布局另
- 下一篇: 山狮来临,Notes何往