使用dispatch_async加速搜索?

时间:2013-05-22 07:29:48

标签: iphone ios objective-c ios6 grand-central-dispatch

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

因此我尝试使用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

答案就像魅力一样:)

8 个答案:

答案 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 ...”),这样您就可以确切地看到错误发生的位置。

在任何一种情况下,您都有两种可能的解决方案:

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

  2. 使用锁(如@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(). 
                    }                        
            }
        }