Как эффективно обновить UITableView с анимацией? - PullRequest
19 голосов
/ 24 января 2011

В моем приложении для iPad есть UITableView, заполненный фидом.Как и большинство читателей RSS, он отображает список ссылок на сообщения в блоге в обратном хронологическом порядке, с их названиями и резюме каждого сообщения.Фид обновляется часто и довольно большой, около 500 сообщений.Я использую libxml2 push parsing для эффективной загрузки и анализа канала в подклассе NSOperation, создания объектов ввода и обновления базы данных на ходу.Но затем мне нужно обновить UITableView с изменениями.

До сих пор приложение обновляло UITableView для каждого проанализированного сообщения, так как оно анализируется.Парсер выполняет селектор в главном потоке, чтобы выполнить эту работу.Но это приводит к некоторой серьезной задержке на пару секунд, если необходимо обновить много ячеек.Я могу смягчить это, запустив обновление таблицы в фоновом потоке, но кажется, что это не очень хорошая идея .Итак, теперь я пытаюсь выяснить, как более эффективно обновить таблицу в главном потоке.

Я мог бы просто позвонить reloadData, когда все сообщения были проанализированы, но это не очень удобно для пользователя: естьнет анимации, указывающей, что что-то изменилось, только вспышка и новые данные.Я бы предпочел, чтобы анимация показывала, что новые сообщения добавляются и старые сообщения удаляются.Существующие сообщения, которые не были удалены из ленты, должны быть перенесены в таблицу новыми сообщениями, появляющимися сверху.

Я знаю, что это возможно. Byline , чтобы привести один пример, делает прекрасную работу.Каждый пост добавляется или удаляется из UITableView по одному за раз без пробелов, показывающих фон таблицы.И все без того, чтобы пользовательский интерфейс ни на что не реагировал.Как это сделать ??

Моя последняя попытка обновить таблицу только после того, как все сообщения были проанализированы (синтаксический анализатор довольно быстрый, так что это не большая задержка).Затем он загружает существующие записи в NSDictionary, сопоставляя их идентификаторы с их индексами в массиве, используемом в качестве источника данных таблицы.Затем он выполняет итерацию по каждому объекту во вновь проанализированном массиве записей, добавляя NSIndexPath для каждого в массивы, которые затем передаются в -insertRowsAtIndexPaths:withRowAnimation:, -deleteRowsAtIndexPaths:withRowAnimation: и -reloadRowsAtIndexPaths:withRowAnimation: в зависимости от ситуации для вставки, удаления, перемещения или обновления ячеек.,Для 500 сообщений обновление занимает около 4 секунд, а пользовательский интерфейс полностью не отвечает.Это время используется почти исключительно для анимированных обновлений UITableView;перебор двух массивов сообщений занимает очень мало времени.

Затем я изменил его так, чтобы они обновлялись без анимации , и у меня есть отдельные массивы для вставки / удаления / перезагрузки только с анимациейдля позиций строк, соответствующих видимым в данный момент строкам.Это лучше, но пропуски появляются при удалении постов и добавлении новых.

Извините, что это так долго, но вот результат:

Как я могу обновить UITableView, с новымячейки включаются, другие выталкиваются, а третьи перемещаются из одной позиции в другую, до 500 ячеек в UITableView (6-8 видны одновременно), и каждая анимация происходит последовательно, все, пока пользовательский интерфейс остается полностьюотзывчивый?

Ответы [ 2 ]

33 голосов
/ 18 февраля 2011

На этот вопрос на самом деле есть три ответа. То есть есть три части этого вопроса:

  1. Как сохранить отзывчивость интерфейса
  2. Как сохранить обновление быстро
  3. Как сделать анимацию обновления таблицы плавной

Отзывчивость интерфейса

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

Это сделано благодаря примеру кода, присланного мне Byline автором Milo Bird, который я затем интегрировал в Dave Dribin DDInvocationGrabber . Этот интерфейс позволяет очень просто поставить в очередь метод, который будет вызван на следующей доступной итерации цикла основного события:

[[(id)delegate queueOnMainThread]
    parserParsedEntries:parsedEntries
               inPortal:parsedPortal];

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

Производительность

Что касается производительности, я изначально обновлял по одной строке UITableView за раз. Это было эффективно, но несколько неэффективно. Я вернулся и изучил пример XMLPerformance , где заметил, что анализатор ждал, пока он соберет 10 элементов, перед отправкой в ​​основной поток для обновления таблицы. Это было ключом к поддержанию производительности без блокировки интерфейса путем обновления всех 500 строк одновременно. Я поиграл с обновлением 1, 10 и всех 500 строк за один вызов, а обновление 10, казалось, предложило лучший компромисс между производительностью и блокировкой пользовательского интерфейса. пять, наверное, тоже неплохо бы сработало.

Анимация

И, наконец, есть анимация. Наблюдая за сеансом «Мастеринг таблиц» WWDC 2010 , я понял, что мое использование методов deleteRowsAtIndexPaths:withRowAnimation: и updateRowsAtIndexPaths:withRowAnimation: было неправильным. Я следил за тем, где что-то должно быть добавлено и удалено в таблице, и корректировал индексы соответствующим образом, но оказывается, что в этом нет необходимости. Внутри блока обновления таблицы нужно только сослаться на индекс строки от до обновления, независимо от того, сколько можно вставить или удалить, чтобы изменить свою позицию. Блок обновлений, по-видимому, делает всю эту бухгалтерию за вас. (Пример ключевого примера приведен к отметке 8:45 в видео).

Таким образом, метод делегата, который обновляет таблицу для количества записей, переданных ей синтаксическим анализатором (в настоящее время 10 по времени), теперь явно отслеживает позиции строк, которые должны быть обновлены или удалены из до блок обновления, вот так:

NSMutableDictionary *oldIndexFor = [NSMutableDictionary dictionaryWithCapacity:posts.count];
int i = 0;
for (PostModel *e in  posts) {
    [oldIndexFor setObject:[NSNumber numberWithInt:i++] forKey:e.ident];
}

NSMutableArray *insertPaths = [NSMutableArray array];
NSMutableArray *deletePaths = [NSMutableArray array];
NSMutableArray *reloadPaths = [NSMutableArray array];
BOOL modified = NO;

for (PostModel *entry in entries) {
    NSNumber *num = [oldIndexFor objectForKey:entry.ident];
    NSIndexPath *path = [NSIndexPath indexPathForRow:currentPostIndex inSection:0];
    if (num == nil) {
        modified = YES;
        [insertPaths addObject:path];
        [posts insertObject:entry atIndex:currentPostIndex];
    } else {
        // Find its current position in the array.
        NSUInteger foundAt = [posts indexOfObject:entry];
        if (foundAt == currentPostIndex) {
            // Reload it if it has changed.
            if (entry.savedState != PostModelSavedStateUnmodified) {
                modified = YES;
                [posts replaceObjectAtIndex:foundAt withObject:entry];
                [reloadPaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
            }
        } else {
            // Move it.
            modified = YES;
            [posts removeObjectAtIndex:foundAt];
            [posts insertObject:entry atIndex:currentPostIndex];
            [insertPaths addObject:path];
            [deletePaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
        }
    }
    currentPostIndex++;
}
if (modified) {
    [tableView beginUpdates];
    [tableView insertRowsAtIndexPaths:insertPaths withRowAnimation:UITableViewRowAnimationTop];
    [tableView deleteRowsAtIndexPaths:deletePaths withRowAnimation:UITableViewRowAnimationBottom];
    [tableView reloadRowsAtIndexPaths:reloadPaths withRowAnimation:UITableViewRowAnimationFade];
    [tableView endUpdates];
}

Комментарии приветствуются. Вполне возможно, что есть более эффективные способы сделать это (использование -[NSArray indexOfObject:] особенно подозрительно для меня) и что я, возможно, пропустил некоторые другие тонкости.

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

0 голосов
/ 24 января 2011

Вы пробовали [tableView beginUpdates]; и [tableView endUpdate];?

...