繁体   English   中英

使用dispatch_async加速搜索?

[英]Speed up search using dispatch_async?

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

所以我试图通过使用dispatch_async而不是dispatch_syncUI上拆分搜索谓词 ,因为如果我使用它没有区别。

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

努力工作后搜索Predicate只用了2ms :)但是当用户搜索时键盘仍然滞后,所以我得到的结果之后我唯一能做的就是重新加载表“改变用户界面”这个我认为让它滞后,

所以我搜索分裂这两个操作“在键盘上打字和刷新UI”。

更新-2-:

@matehat https://stackoverflow.com/a/16879900/1658442

@TomSwift https://stackoverflow.com/a/16866049/1658442

答案像魅力一样工作:)

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

因此,在后台线程上,您应首先过滤到单独的临时数组。 然后在主线程searchArray临时数组分配给searchArray

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

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

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

一种解决方案可能是自愿引发搜索之间的延迟以让用户键入并让搜索异步执行。 这是如何做:

首先确保您的队列创建如下:

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 ms内安排搜索
  • 同时,任何新的搜索请求都不会被处理,因为搜索将在几毫秒内完成
  • 当计划的搜索发生时, _scheduledSearch ivar重置为FALSE ,因此处理下一个请求。

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

首先,关于您提供的代码的几个注释:

1)看起来好像你可能在用户输入时排队多次搜索,这些都必须在相关的一个(最近的一个)用所需的结果集更新显示之前运行完成。

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

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

无论您是否使用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;
    }
}

我相信你的崩溃确实是通过嵌入UI元素的显示来解决的,其中searchArray是在另一个调用中调用GrandCentralDispatch的后备元素(如您在更新的原始帖子中所示)。 这是确保您不会导致数组元素在幕后更改的唯一方法,同时显示与其关联的项目。

但是,我相信如果你看到滞后,它不是由于2ms的阵列处理或需要30ms的重新加载造成的,而是由GCD到达主队列上的内部dispatch_sync调用的时间造成的。 。

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

你可能在其他地方滞后(即如果你通过尝试去网络获得结果来增加搜索结果,你可能想要进行调用,然后在你的单独的调度队列上处理响应),但是对于你来说正在谈论,这一点处理不需要拆分到不同的队列。 对于任何超过30ms的核心处理,你应该考虑GCD。

尝试以下一种方式修改您的功能:

功能原型;

- (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];
     });
 }];
});

希望它能帮到你)

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

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

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

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

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

我找到了一个简单的解决方案,具有与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秒而不键入任何其他内容时才会执行。 相反,如果用户输入的速度足够快,则不会进行任何查询。 解决方案可以在有或没有线程的情况下完成。

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(). 
                    }                        
            }
        }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM