我正在尝试加快我的应用搜索速度,但是当有大量数据时它会滞后。
因此我尝试使用dispatch_async
而不是dispatch_sync
在用户界面上拆分搜索谓词,因为如果我使用它,则没有任何不同。< / p>
问题是,当我使用dispatch_async
时,应用有时因[__NSArrayI objectAtIndex:]: index "17" beyond bounds
而崩溃。
我现在发生这种情况是因为我们说第一个仍然可以工作并重新加载tableView并继续搜索会改变数组大小取决于结果所以在这种情况下“CRASH”:(
这是我的代码:
dispatch_async(myQueue, ^{
searchArray = [PublicMeathods searchInArray:searchText array:allData];
} );
if(currentViewStyle==listViewStyle){
[mytable reloadData];
}
我试过这个:
dispatch_async(myQueue, ^{
NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
dispatch_sync(dispatch_get_main_queue(), ^{
searchArray = tmpArray;
[mytable reloadData];
});
});
但在这种情况下,滞后仍然存在。
更新-1-:
在努力工作后,搜索谓词只需2毫秒:) 但是当用户搜索时键盘仍然滞后,所以我得到结果后唯一能做的就是重新加载表“改变UI”,这就是我认为让它滞后,
所以我搜索分割这两个操作“在键盘上键入&刷新UI”。
更新-2-:
@matehat https://stackoverflow.com/a/16879900/1658442
和
@TomSwift https://stackoverflow.com/a/16866049/1658442
答案就像魅力一样:)
答案 0 :(得分:9)
如果searchArray
是用作表视图数据源的数组,那么此数组必须
只能在主线程上访问和修改。
因此,在后台线程中,您应首先过滤到单独的临时数组。然后在主线程上将临时数组分配给searchArray
:
dispatch_async(myQueue, ^{
NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
dispatch_sync(dispatch_get_main_queue(), ^{
searchArray = tmpArray;
[mytable reloadData];
});
});
更新:使用临时数组可以解决崩溃问题,使用后台线程有助于在搜索过程中保持UI响应。但正如在讨论中发现的那样,搜索速度缓慢的一个主要原因可能是复杂的搜索逻辑。
可能有助于存储额外的“规范化”数据(例如,所有数据都转换为小写,电话号码转换为标准格式等等),以便实际搜索可以完成 更快的不区分大小写的比较。
答案 1 :(得分:4)
一种解决方案可能是自愿在搜索之间引发延迟,让用户输入并让搜索异步执行。方法如下:
首先确保您的队列创建如下:
dispatch_queue_t myQueue = dispatch_queue_create("com.queue.my", DISPATCH_QUEUE_CONCURRENT);
在您的班级中定义此ivar(并在初始化时将其设置为FALSE
):
BOOL _scheduledSearch;
将此宏记录在文件的顶部(或任何地方,确保其可见)
#define SEARCH_DELAY_IN_MS 100
而不是您的第二个代码段,请调用此方法:
[self scheduleSearch];
其实施是:
- (void) scheduleSearch {
if (_scheduledSearch) return;
_scheduledSearch = YES;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)((double)SEARCH_DELAY_IN_MS * NSEC_PER_MSEC));
dispatch_after(popTime, myQueue, ^(void){
_scheduledSearch = NO;
NSString *searchText = [self textToSearchFor];
NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
dispatch_async(dispatch_get_main_queue(), ^{
searchArray = tmpArray;
[mytable reloadData];
});
if (![[self textToSearchFor] isEqualToString:searchText])
[self scheduleSearch];
});
}
[self textToSearchFor]
是您应该从中获取实际搜索文本的地方。
以下是它的作用:
_scheduledSearch
ivar设置为TRUE
,并告知GCD在100毫秒内安排搜索_scheduledSearch
ivar会重置为FALSE
,因此会处理下一个请求。您可以使用SEARCH_DELAY_IN_MS
的不同值来满足您的需求。此解决方案应完全将键盘事件与搜索生成的工作负载分离。
答案 2 :(得分:3)
首先,关于您提供的代码的几个注释:
1)看起来您可能会在用户输入时排队多次搜索,并且必须在相关的(最近的)更新显示之前运行完成所需的结果集
2)您显示的第二个片段是线程安全方面的正确模式。第一个代码段在搜索完成之前更新UI。可能你的崩溃发生在第一个片段,因为后台线程在主线程从中读取时更新searchArray,这意味着你的数据源(由searchArray支持)处于不一致状态。
如果你不使用UISearchDisplayController
,你就不会说,这并不重要。但如果你是,一个常见的问题是没有实现- (BOOL) searchDisplayController: (UISearchDisplayController *) controller shouldReloadTableForSearchString: (NSString *) filter
并返回NO。通过实现此方法并返回NO,您将关闭每次更改搜索项时重新加载tableView的默认行为。相反,您有机会启动新术语的异步搜索,并且只有在获得新结果后才更新UI([tableview reloadData]
)。
无论您是否使用UISearchDisplayController
,在实施异步搜索时都需要考虑以下几点:
1)理想情况下,如果搜索不再有用(例如搜索字词已更改),您可以中断正在进行的搜索并取消搜索。您的“searchInArray”&#39;方法似乎不支持这一点。但是如果你只是扫描一个阵列,这很容易做到。
1a)如果无法取消搜索,您仍需要在搜索结束时查看结果是否相关。如果没有,则不要更新用户界面。
2)搜索应该在后台线程上运行,以免陷入主线程和UI。
3)搜索完成后,需要更新主线程上的UI(以及UI的数据源)。
我将示例项目(here, on Github)放在一起,对大量单词列表执行效率非常低的搜索。当用户在他们的术语中键入时,UI保持响应,并且当它们变得无关紧要时,衍生的搜索会自行取消。样本的内容是这段代码:
- (BOOL) searchDisplayController: (UISearchDisplayController *) controller
shouldReloadTableForSearchString: (NSString *) filter
{
// we'll key off the _currentFilter to know if the search should proceed
@synchronized (self)
{
_currentFilter = [filter copy];
}
dispatch_async( _workQueue, ^{
NSDate* start = [NSDate date];
// quit before we even begin?
if ( ![self isCurrentFilter: filter] )
return;
// we're going to search, so show the indicator (may already be showing)
[_activityIndicatorView performSelectorOnMainThread: @selector( startAnimating )
withObject: nil
waitUntilDone: NO];
NSMutableArray* filteredWords = [NSMutableArray arrayWithCapacity: _allWords.count];
// only using a NSPredicate here because of the SO question...
NSPredicate* p = [NSPredicate predicateWithFormat: @"SELF CONTAINS[cd] %@", filter];
// this is a slow search... scan every word using the predicate!
[_allWords enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
// check if we need to bail every so often:
if ( idx % 100 == 0 )
{
*stop = ![self isCurrentFilter: filter];
if (*stop)
{
NSTimeInterval ti = [start timeIntervalSinceNow];
NSLog( @"interrupted search after %.4lf seconds", -ti);
return;
}
}
// check for a match
if ( [p evaluateWithObject: obj] )
{
[filteredWords addObject: obj];
}
}];
// all done - if we're still current then update the UI
if ( [self isCurrentFilter: filter] )
{
NSTimeInterval ti = [start timeIntervalSinceNow];
NSLog( @"completed search in %.4lf seconds.", -ti);
dispatch_sync( dispatch_get_main_queue(), ^{
_filteredWords = filteredWords;
[controller.searchResultsTableView reloadData];
[_activityIndicatorView stopAnimating];
});
}
});
return FALSE;
}
- (BOOL) isCurrentFilter: (NSString*) filter
{
@synchronized (self)
{
// are we current at this point?
BOOL current = [_currentFilter isEqualToString: filter];
return current;
}
}
答案 3 :(得分:1)
我相信你的崩溃确实是通过嵌入UI元素的显示来解决的,其中searchArray是在另一个调用中调用GrandCentralDispatch的后备元素(如您在更新的原始帖子中所示)。这是确保在显示与之关联的项目时,不会导致数组元素在幕后更改的唯一方法。
然而,我相信如果你看到滞后,它不是由于2ms的阵列处理或者需要30ms的重新加载造成的,而是由GCD到达内部dispatch_sync调用的时间引起的。主要队列。
如果,到目前为止,你已经设法在最坏的情况下将阵列的处理时间缩短到2毫秒(或者即使你已经设法将其降低到不到30毫秒,这大约是它的时间需要以30 fps的速度处理主运行循环中的帧,然后你应该考虑完全放弃GCD来处理这个数组。在主队列上花费2ms来处理你的数组不会导致任何错误的行为。
你可能在其他地方滞后(例如,如果你通过试图通过网络获得结果来增加搜索结果,你可能想要进行调用然后在单独的调度队列上处理响应),但对于在你谈论的时候,这一点处理不需要分成不同的队列。对于任何超过30ms的核心处理,你应该考虑GCD。
答案 4 :(得分:1)
尝试以下一种方式修改您的功能:
函数原型;
- (void)searchInArray:searchText array:allData complete: (void (^)(NSArray *arr)) complete;
功能本身
- (void)searchInArray:searchText array:allData complete: (void (^)(NSArray *arr)) complete {
NSArray * array = [NSArray new];
// function code
complete(array)//alarming that we have done our stuff
}
当你调用这个函数时
dispatch_queue_t searchQueue = dispatch_queue_create("com.search",NULL);
dispatch_async(searchQueue,^{
[PublicMeathods searchInArray:searchText array:allData complete:^(NSArray *arr) {
searchArray = arr;
dispatch_async(dispatch_get_main_queue(), ^{
[myTable reloadData];
});
}];
});
希望它能帮到你)
答案 5 :(得分:1)
我怀疑你的问题是主队列和后台队列之间共享allData
。如果在主队列中对allData
进行更改,则可能会缩短后台队列中的allData
,从而导致过去有效的索引无效。
问题也可能不是allData
本身,而是allData
中对象中的某些数组。尝试在异常上设置断点(在Xcode中,打开Breakpoints源列表,单击底部的加号按钮,然后选择“Add Exception Breakpoint ...”),这样您就可以确切地看到错误发生的位置。
在任何一种情况下,您都有两种可能的解决方案:
在搜索中使用之前复制有问题的对象。这可以保护后台队列免受主队列中的更改,但是根据您需要复制的内容,可能很难将更改返回到UI中 - 您可能必须将副本与原始队列进行匹配。
使用锁(如@synchronized
)或每对象队列来确保一次只有一个队列正在使用该对象。 NSManagedObjectContext
将-performBlock:
和-performBlockAndWait:
方法用于后一种方法。但是,在不阻塞主队列的情况下执行此操作可能有点棘手。
答案 6 :(得分:1)
我找到了一个简单的解决方案,具有与Matehad提出的解决方案相同的精神(等待一段时间并仅在用户不输入任何其他内容时执行搜索)。这是:
声明2个全局计数器和一个全局字符串:
int keyboardInterruptionCounter1 = 0,int keyboardInterruptionCounter2 = 0和NSString * searchTextGlobal
在searchBar函数上执行以下操作:
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
keyboardInterruptionCounter1++;
searchTextGlobal = searchText;//from local variable to global variable
NSTimeInterval waitingTimeInSec = 1;//waiting time according to typing speed.
//waits for the waiting time
[NSTimer scheduledTimerWithTimeInterval:waitingTimeInSec target:self selector:@selector(timerSearchBar:) userInfo:nil repeats:NO];
}
-(void)timerSearchBar:(NSTimer *)timer{
keyboardInterruptionCounter2++;
// enters only if nothing else has been typed.
if (keyboardInterruptionCounter2 == keyboardInterruptionCounter1) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
//do the search with searchTextGlobal string
dispatch_async(dispatch_get_main_queue(), ^{
//update UI
});
});
}
}
说明:仅当两个计数器相同时才执行搜索,这仅在用户键入并等待.52秒而不键入任何其他内容时才会执行。相反,如果用户输入的速度足够快,则不会进行任何查询。解决方案可以在有或没有线程的情况下完成。
答案 7 :(得分:0)
Martin R发布了一个正确答案。唯一要指出的是,而不是
dispatch_sync(dispatch_get_main_queue()
应该是
dispatch_async(dispatch_get_main_queue()
Swift中的完整代码是:
let remindersFetcherQueue = dispatch_queue_create("com.gmail.hillprincesoftware.remindersplus", DISPATCH_QUEUE_CONCURRENT)
dispatch_sync(remindersFetcherQueue) {
println("Start background queue")
estore.fetchRemindersMatchingPredicate(remindersPredicate) {
reminders in
// var list = ... Do something here with the fetched reminders.
dispatch_async(dispatch_get_main_queue()) {
self.list = list // Assign to a class property
self.sendChangedNotification() // This should send a notification which calls a function to ultimately call setupUI() in your view controller to do all the UI displaying and tableView.reloadData().
}
}
}