Ускорить поиск с помощью dispatch_async?

Я пытаюсь ускорить поиск своего приложения, он становится отстающим, когда есть много данных.

поэтому я пытаюсь разделить поиск Predicate на UI с помощью dispatch_async not dispatch_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 мс:) после тяжелой работы:) но клавиатура все еще отстает, когда пользователь выполняет поиск, поэтому единственное, что я делаю после получения результата, - это перезагрузка таблицы "изменение в пользовательском интерфейсе", что, по-моему, делает ее отстающей,

Итак, что я ищу для разделения этой двух операций "набрав на клавиатуре и обновить интерфейс".

Обновление -2-:

@matehat qaru.site/info/539455/...

и

@TomSwift qaru.site/info/539455/...

ответы работают как шарм:)

Ответ 1

Одним из решений может быть добровольное побуждение к задержке между запросами, чтобы пользователь вводил тип и позволял выполнять асинхронный поиск. Вот как:

Сначала убедитесь, что ваша очередь создана следующим образом:

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 reset to FALSE, поэтому выполняется следующий запрос.

Вы можете играть с различными значениями для SEARCH_DELAY_IN_MS, чтобы они соответствовали вашим потребностям. Это решение должно полностью отменить события клавиатуры с рабочей нагрузкой, полученной при поиске.

Ответ 2

Если searchArray - это массив, который используется как источник данных таблицы, тогда этот массив должен только доступ и изменение в основном потоке.

Поэтому в фоновом потоке сначала необходимо сначала отфильтровать отдельный временной массив. Затем вы назначаете временный массив searchArray в основном потоке:

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

Обновление. Использование временного массива должно решить проблему сбоя, а использование фонового потока помогает поддерживать отзывчивость пользовательского интерфейса во время поиска. Но, как оказалось в обсуждении, основной причиной медленного поиска может быть сложная логика поиска.

Это может помочь сохранить дополнительные "нормализованные" данные (например, все конвертированные в нижний регистр, номера телефонов, преобразованные в стандартную форму и т.д.), так что фактический поиск может быть выполнен с помощью более быстрое сравнение без учета регистра.

Ответ 3

Во-первых, пара примечаний по представленному вами коду:

1) Похоже, что вы, вероятно, ставите в очередь несколько поисковых запросов по типу пользователя, и все они должны выполняться до завершения, прежде чем соответствующий (последний) обновит отображение с желаемым набором результатов.

2) Второй фрагмент, который вы показываете, - это правильный шаблон с точки зрения безопасности потоков. Первый фрагмент обновляет пользовательский интерфейс перед завершением поиска. Вероятно, ваш сбой происходит с первым фрагментом, потому что фоновый поток обновляет searchArray, когда основной поток читает из него, что означает, что ваш источник данных (поддерживаемый searchArray) находится в несогласованном состоянии.

Вы не говорите, используете ли вы UISearchDisplayController или нет, и это действительно не имеет значения. Но если вы, одна общая проблема не реализует - (BOOL) searchDisplayController: (UISearchDisplayController *) controller shouldReloadTableForSearchString: (NSString *) filter и возвращает NO. Внедряя этот метод и возвращая NO, вы отключите поведение по умолчанию для перезагрузки tableView с каждым изменением в поисковый запрос. Вместо этого у вас есть возможность начать асинхронный поиск нового термина и обновить интерфейс ([tableview reloadData]) только после получения новых результатов.

Независимо от того, используете ли вы UISearchDisplayController, при реализации асинхронного поиска необходимо учитывать несколько вещей:

1) В идеале вы можете прервать поиск в процессе и отменить его, если поиск больше не полезен (например, изменился термин поиска). Ваш метод searchInArray не поддерживает это. Но это легко сделать, если вы просто сканируете массив.

1a) Если вы не можете отменить поиск, вам по-прежнему нужен способ в конце поиска, чтобы узнать, соответствуют ли ваши результаты или нет. Если нет, то не обновляйте пользовательский интерфейс.

2) Поиск должен выполняться в фоновом потоке, чтобы не прогнать основной поток и пользовательский интерфейс.

3) По завершении поиска необходимо обновить UI (и источник данных UI) в основном потоке.

Я собрал образец проекта (здесь, на Github), который выполняет довольно неэффективный поиск по большому списку слов. Пользовательский интерфейс остается отзывчивым по мере того, как пользователь вводит в свой срок, и порожденные поиски отменяют себя, поскольку они становятся неуместными. Мяч образца - это код:

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

Ответ 4

Я считаю, что ваш сбой действительно разрешен вложением отображения элемента интерфейса, для которого searchArray является элементом поддержки в вызове GrandCentralDispatch внутри другого вызова (как вы видите в обновленном исходном сообщении). это единственный способ убедиться, что элементы массива не изменяются за кулисами, пока происходит отображение связанных с ним элементов.

Тем не менее, я считаю, что если вы видите отставание, это не столько вызвано обработкой массива в 2 мс, либо перезагрузкой, которая занимает 30 мс, а скорее к тому времени, когда требуется, чтобы GCD попал во внутренний вызов dispatch_sync основная очередь.

если к этому моменту вам удастся обработать ваш массив до 2 мс в худшем случае (или даже если вам удастся снизить его до менее 30 мс, что примерно соответствует времени принимает для обработки кадра в цикле основного запуска со скоростью 30 кадров в секунду), то вам следует отказаться от GCD вообще в ваших усилиях по обработке этого массива. взятие 2ms в основной очереди для обработки вашего массива не вызовет каких-либо ошибок.

у вас может быть отставание в другом месте (т.е. если вы увеличиваете результаты поиска, пытаясь выйти в сеть, чтобы получить результаты, вы можете выполнить вызов, а затем обработать ответ в отдельной очереди отправки), но для время, о котором вы говорите, этот бит обработки не нужно разделить на отдельные очереди. для любой жесткой обработки, которая занимает более 30 мс, вы должны рассмотреть GCD.

Ответ 5

Попробуйте изменить свои функции следующим образом:

прототип функции;

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

Надеюсь, это вам поможет)

Ответ 6

Я подозреваю, что ваша проблема заключается в том, что allData разделяется между основной и фоновой очередями. Если вы сделаете изменение в allData в главной очереди, это может сократить allData в фоновом порядке, в результате чего индекс, который раньше был действительным, стал недействительным.

Также возможно, что проблема не сама allData, а некоторый массив внутри объектов в allData. Попробуйте установить точку останова на исключениях (в Xcode, откройте список источников точек останова, нажмите кнопку "плюс" внизу и выберите "Добавить точку останова исключений..." ), чтобы вы могли точно видеть, где происходит ошибка.

В любом случае у вас есть два возможных решения:

  • Скопируйте нарушающий объект, прежде чем использовать его в поиске. Это защищает фоновую очередь от изменений в основной очереди, но в зависимости от того, что вам нужно скопировать, может быть сложно вернуть изменения в пользовательский интерфейс - возможно, вам придется сопоставить копии с их оригиналами.

  • Используйте блокировку (например, @synchronized) или очередь для каждого объекта, чтобы гарантировать, что только одна очередь использует объект за раз. NSManagedObjectContext использует последний подход для своих методов -performBlock: и -performBlockAndWait:. Это может быть немного сложно сделать, не блокируя основную очередь.

Ответ 7

Я нашел простое решение с тем же духом решения, которое предлагает 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 с без ввода чего-либо еще. Вместо этого, если пользователи набирают достаточно быстро, то запрос не выполняется. Решение может быть выполнено с помощью или без нарезки.

Ответ 8

Мартин Р опубликовал правильный ответ. Единственное, что нужно отметить, вместо
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(). 
                    }                        
            }
        }