10

我正在尝试加快我的应用搜索速度,当有大量数据时它会出现滞后。

所以我试图在UI上拆分搜索谓词,如果我使用它,不会有任何不同。dispatch_asyncdispatch_sync

问题是当我使用时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

答案就像一个魅力:)

4

8 回答 8

9

如果searchArray是用作表视图数据源的数组,则只能在主线程上访问和修改该数组。

因此,在后台线程上,你应该先过滤成一个单独的临时数组。然后将临时数组分配给searchArray主线程:

dispatch_async(myQueue, ^{
    NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
    dispatch_sync(dispatch_get_main_queue(), ^{
        searchArray = tmpArray;
        [mytable reloadData];
    });
});

更新:使用临时数组应该可以解决崩溃问题,并且使用后台线程有助于在搜索期间保持 UI 响应。但正如讨论中发现的那样,搜索速度慢的一个主要原因可能是复杂的搜索逻辑。

它可能有助于存储额外的“规范化”数据(例如,全部转换为小写,电话号码转换为标准形式等),以便可以通过更快的不区分大小写的比较来完成实际搜索。

于 2013-05-22T07:35:32.270 回答
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]是您应该从中获取实际搜索文本的位置。

这是它的作用:

  • 第一次收到请求时,它会将_scheduledSearchivar 设置为TRUE并告诉 GCD 在 100 毫秒内安排一次搜索
  • 同时,不会处理任何新的搜索请求,因为无论如何搜索都会在几毫秒内发生
  • 当预定搜索发生时,_scheduledSearchivar 被重置为FALSE,因此处理下一个请求。

您可以使用不同的值SEARCH_DELAY_IN_MS来满足您的需求。此解决方案应将键盘事件与搜索产生的工作负载完全分离。

于 2013-06-02T04:53:53.497 回答
4

首先,关于您提供的代码的几点说明:

1) 看起来您可能会在用户键入时将多个搜索排队,并且这些都必须在相关搜索(最近的搜索)使用所需结果集更新显示之前运行完成。

2)您展示的第二个片段是线程安全方面的正确模式。第一个片段在搜索完成之前更新 UI。第一个片段可能会发生崩溃,因为当主线程从它读取时,后台线程正在更新 searchArray,这意味着您的数据源(由 searchArray 支持)处于不一致的状态。

你没有说你是否使用a UISearchDisplayController,这真的没关系。但如果你是,一个常见的问题是没有实施- (BOOL) searchDisplayController: (UISearchDisplayController *) controller shouldReloadTableForSearchString: (NSString *) filter并返回 NO。通过实现此方法并返回 NO,您将关闭在每次更改搜索词时重新加载 tableView 的默认行为。相反,您有机会开始异步搜索新术语,并[tableview reloadData]仅在获得新结果后更新 UI ( )。

无论您是否使用UISearchDisplayController,在实现异步搜索时都需要考虑一些事项:

1) 理想情况下,如果搜索不再有用(例如搜索词已更改),您可以中断正在进行的搜索并取消它。您的“searchInArray”方法似乎不支持这一点。但是,如果您只是扫描一个阵列,这很容易做到。

1a) 如果您无法取消您的搜索,您仍然需要在搜索结束时查看您的结果是否相关。如果没有,则不要更新 UI。

2) 搜索应该在后台线程上运行,以免阻塞主线程和 UI。

3) 一旦搜索完成,它需要在主线程上更新 UI(和 UI 的数据源)。

我将示例项目(这里,在 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;
    }
}
于 2013-05-31T21:10:58.830 回答
1

我相信您的崩溃确实可以通过嵌入 UI 元素的显示来解决,其中 searchArray 是另一个调用内部对 GrandCentralDispatch 的调用中的支持元素(如您在更新的原始帖子中所示)。这是确保在显示与之关联的项目时不会导致数组元素在幕后发生变化的唯一方法。

但是,我相信如果您看到延迟,这并不是由 2ms 的数组处理或需要 30ms 的重新加载引起的,而是由 GCD 到达主队列上的内部 dispatch_sync 调用所引起的.

如果到此为止,您已经设法在最坏的情况下将阵列的处理时间缩短到只有 2 毫秒(或者即使您设法将其降低到不到 30 毫秒,这大约是处理所需的时间30 fps 的主运行循环中的一个帧),那么您应该考虑完全放弃 GCD 来处理这个数组。在主队列上花费 2 毫秒来处理您的数组不会导致任何错误行为。

您可能在其他地方滞后(即,如果您通过尝试上网获取结果来增加搜索结果,您可能希望进行调用,然后在单独的调度队列上处理响应),但是对于您正在谈论,这部分处理不需要拆分到单独的队列中。对于任何超过 30 毫秒的硬核处理,您应该考虑 GCD。

于 2013-05-27T15:16:15.407 回答
1

我怀疑您的问题是allData在主队列和后台队列之间共享。如果您allData对主队列进行更改,则可能会allData在后台队列中缩短,导致曾经有效的索引变为无效。

问题也可能不是allData本身,而是allData. 尝试在异常上设置断点(在 Xcode 中,打开 Breakpoints 源列表,单击底部的加号按钮,然后选择“Add Exception Breakpoint...”),这样您就可以准确地看到错误发生的位置。

无论哪种情况,您都有两种可能的解决方案:

  1. 在搜索中使用它之前复制有问题的对象。这可以保护后台队列免受主队列中的更改,但根据您需要复制的内容,可能很难将更改恢复到 UI 中 - 您可能必须将副本与原始队列匹配。

  2. 使用锁(如@synchronized)或每个对象队列来确保一次只有一个队列在使用该对象。NSManagedObjectContext对其-performBlock:和方法使用后一种-performBlockAndWait:方法。但是,在不阻塞主队列的情况下执行此操作可能有点棘手。

于 2013-05-31T21:32:11.570 回答
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];
     });
 }];
});

希望对你有帮助)

于 2013-05-30T12:22:29.000 回答
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
     });

    });

    }
}

说明:仅当两个计数器相同时才执行搜索,仅当用户键入并等待 0.52 秒而没有键入任何其他内容时才会发生这种情况。相反,如果用户键入的速度足够快,则不会进行任何查询。该解决方案可以使用或不使用线程来完成。

于 2014-02-20T17:43:26.253 回答
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(). 
                    }                        
            }
        }
于 2015-04-05T10:48:23.907 回答