最近简单优化了下项目内一些数据搜索功能。
背景是用户的关注列表和群聊成员都太多了,项目早期的搜索逻辑完全不顶用。搜索的内容包括三大块:
命中的逻辑比较粗暴,目标字符串对搜索词的精准匹配和拼音模糊匹配,具体而言:
命中后需要将对应的字符高亮,比如输入「M」命中「米饭」后,「米」字需要高亮。具体实现算法,网路上已经有很多讨论,这里只聊下比较通用的、充分利用资源的搜索逻辑优化。
缓存结果其实很好理解,比如面对如下 4 条数据的时候:
Dinesh Carlos
Earl Fletcher
Dash Marnie
Finbar Alastair
输入「D」可以命中到两条结果:
_D_inesh Carlos
_D_ash Marnie
然后就输入「a」,那么只从刚才到两条结果中继续搜索两次即可,命中:
_Da_sh Marnie
针对顺序输入字符串的情况写了简陋的搜索类实现这个功能 —— CIRSearcher。里面维护缓存的时候呢,没有直接采用数组存储对象的方式,而是使用了 bit array 记录命中索引的结果。比如 2000 个数据,输入第一个字符后命中 1000 个,如果使用 NSArray
存储对象,那么则需要:
retain
消息;相比较而言,bit array 记录索引:
retain
对象,命中的对象只要在 bit array 中的对应 bit 位置 1;当然这是优势情况下的描述,毕竟 bit array 的空间是固定的,如果结果较少,bit array 反而会占用太多空间。当然这些都是可以根据业务性质去衡量、决策。缓存中间结果也会带来问题:在业务中,通常会在搜索逻辑的执行时,记录高亮字符串,那么当用户回退输入时,直接返回上一次结果的话,那么此时的高亮字符依然是回退前的。解决这个问题,可以把搜索逻辑再执行一次,或者让匹配逻辑自己保存都行。
大量的数据处理,其实可以通过拆分任务,然后并发执行,最后统一组装结果返回。给 NSArray
加一个扩展:
- (NSArray *)kt_perform:(NSArray *(^)(NSArray *))execute {
NSAssert(execute, @"no execute block");
static NSInteger processorCount = 0;
if (processorCount == 0) {
processorCount = [NSProcessInfo processInfo].processorCount;
}
NSInteger preset = processorCount / 2 + 1;
NSInteger numberOfExecution = MIN(self.count, preset);
NSInteger groupCount = self.count / numberOfExecution;
NSInteger remain = self.count % numberOfExecution;
NSMutableArray<NSArray *> *resultArrayToReplace = [[NSMutableArray alloc] initWithCapacity:numberOfExecution];
NSArray *placeholder = [NSArray new];
for (int i = 0; i < numberOfExecution; i++) {
[resultArrayToReplace addObject:placeholder];
}
__block NSInteger totoalResultCount = 0;
dispatch_apply(numberOfExecution, DISPATCH_APPLY_AUTO, ^(size_t i) {
NSInteger startLoc = i * groupCount;
NSInteger length = groupCount;
if (i == (numberOfExecution - 1)) {
length += remain;
}
NSArray *result = execute([self subarrayWithRange:NSMakeRange(startLoc, length)]);
totoalResultCount += result.count;
[resultArrayToReplace replaceObjectAtIndex:i withObject:result];
});
NSMutableArray *finalResult = [[NSMutableArray alloc] initWithCapacity:totoalResultCount];
[resultArrayToReplace enumerateObjectsUsingBlock:^(NSArray * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[finalResult addObjectsFromArray:obj];
}];
return finalResult;
}
上面的代码其实很简单,根据设备的状况,决定将这个数据分成多少子数组,然后用 libdispatch 提供的并发 API 执行 execute block。注意 resultArrayToReplace
采用占位替换的方式存储数据,而不是空数组添加,一个是为了保证顺序,更重要的是保证线程安全,因为 backend storage 不再需要 realloc/free 空间。这点上,前文提到的 bit array 也是一样的,同样是线程安全的。
— Jul 11, 2021