Последний из первых в стеке с GCD? - PullRequest
28 голосов
/ 27 сентября 2011

У меня есть UITableView, который отображает изображения, связанные с контактами в каждой строке.В некоторых случаях эти изображения читаются на первом дисплее из изображения контакта в адресной книге, и там, где его нет, они представляют собой аватар, отображаемый на основе сохраненных данных.В настоящее время эти изображения обновляются в фоновом потоке с помощью GCD.Однако это загружает изображения в том порядке, в котором они были запрошены, что означает, что во время быстрой прокрутки очередь становится длинной, а когда пользователь прекращает прокручивать, текущие ячейки являются последними для обновления.На iPhone 4 проблема не очень заметна, но я очень заинтересован в поддержке старого оборудования и тестирую на iPhone 3G.Задержка допустима, но весьма заметна.

Мне кажется, что стек Last In-First Out, вероятно, в значительной степени решит эту проблему, поскольку всякий раз, когда пользователь прекращает прокручивать эти ячейки, он будет следующим, который будет обновлен итогда остальные, которые в данный момент находятся за пределами экрана, будут обновлены.Это возможно с Grand Central Dispatch?Или не слишком обременительно для реализации каким-либо другим способом?

Обратите внимание, кстати, что я использую Core Data с хранилищем SQLite, и я не использую NSFetchedResultsController из-за многих ко многимотношения, которые необходимо пройти, чтобы загрузить данные для этого представления.(Насколько мне известно, это исключает использование NSFetchedResultsController.) [Я обнаружил, что NSFetchedResultsController можно использовать с отношениями «многие ко многим», несмотря на то, что официальная документация говорит.Но я пока не использую его в этом контексте.]

Добавление: Просто хочу отметить, что в то время как тема "Как создать последний поступивший первым"Stack with GCD ", на самом деле я просто хочу решить проблему, изложенную выше, и, возможно, есть лучший способ сделать это.Я более чем открыт для предложений, подобных предложению Timthetoolman, которое решает проблему, изложенную по-другому;если, наконец, я воспользуюсь таким предложением, я признаю как лучший ответ на исходный вопрос, так и лучшее решение, которое я в итоге реализовал ... :)

Ответы [ 8 ]

16 голосов
/ 22 октября 2011

Из-за ограничений памяти устройства вы должны загружать изображения по требованию и в фоновую очередь GCD. В cellForRowAtIndexPath: метод проверьте, чтобы видеть, является ли изображение вашего контакта нулевым или было кэшировано. Если изображение отсутствует или отсутствует в кэше, используйте вложенный dispatch_async для загрузки изображения из базы данных и обновления ячейки tableView.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
   {
       static NSString *CellIdentifier = @"Cell";
       UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
       if (cell == nil) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
       }
       // If the contact object's image has not been loaded, 
       // Use a place holder image, then use dispatch_async on a background queue to retrieve it.

       if (contact.image!=nil){
           [[cell imageView] setImage: contact.image];
       }else{
           // Set a temporary placeholder
           [[cell imageView] setImage:  placeHolderImage];

           // Retrieve the image from the database on a background queue
           dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
           dispatch_async(queue, ^{
               UIImage *image = // render image;
               contact.image=image;

               // use an index path to get at the cell we want to use because
               // the original may be reused by the OS.
               UITableViewCell *theCell=[tableView cellForRowAtIndexPath:indexPath];

               // check to see if the cell is visible
               if ([tableView visibleCells] containsObject: theCell]){
                  // put the image into the cell's imageView on the main queue
                  dispatch_async(dispatch_get_main_queue(), ^{
                     [[theCell imageView] setImage:contact.image];
                     [theCell setNeedsLayout];
                  });
               }
           }); 
       }
       return cell;
}

Видео конференции WWDC2010 "Представление блоков и Grand Central Dispatch" демонстрирует пример использования вложенного dispatch_async.

другой потенциальной оптимизацией может стать загрузка изображений в фоновую очередь с низким приоритетом при запуске приложения. т.е.

 // in the ApplicationDidFinishLaunchingWithOptions method
 // dispatch in on the main queue to get it working as soon
 // as the main queue comes "online".  A trick mentioned by
 // Apple at WWDC

 dispatch_async(dispatch_get_main_queue(), ^{
        // dispatch to background priority queue as soon as we
        // get onto the main queue so as not to block the main
        // queue and therefore the UI
        dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
        dispatch_apply(contactsCount,lowPriorityQueue ,^(size_t idx){
               // skip the first 25 because they will be called
               // almost immediately by the tableView
               if (idx>24){
                  UIImage *renderedImage =/// render image
                  [[contactsArray objectAtIndex: idx] setImage: renderedImage];
               }

        });
 });

С помощью этой вложенной отправки мы визуализируем изображения в очереди с чрезвычайно низким приоритетом. Помещение рендеринга изображений в очередь с приоритетом фона позволит отображать изображения с помощью метода cellForRowAtIndexPath, приведенного выше, с более высоким приоритетом. Таким образом, из-за разницы в приоритетах очередей у ​​вас будет LIFO «бедняги».

Удачи.

11 голосов
/ 30 октября 2011

Приведенный ниже код создает гибкий стек «последний пришел - первым вышел», который обрабатывается в фоновом режиме с помощью Grand Central Dispatch. Класс SYNStackController является универсальным и может использоваться повторно, но этот пример также предоставляет код для варианта использования, указанного в вопросе, асинхронно отображая изображения ячеек таблицы и гарантируя, что после быстрой прокрутки будут отображаться следующие отображаемые ячейки.

Благодарность Бен М. , чей ответ на этот вопрос предоставил исходный код, на котором это основывалось. (В его ответе также содержится код, который можно использовать для проверки стека.) Представленная здесь реализация не требует ARC и использует только Grand Central Dispatch, а не executeSelectorInBackground. В приведенном ниже коде также хранится ссылка на текущую ячейку с использованием objc_setAssociatedObject, что позволит связать визуализированное изображение с правильной ячейкой, когда изображение впоследствии загружается асинхронно. Без этого кода изображения, созданные для предыдущих контактов, будут неправильно вставляться в повторно используемые ячейки, даже если они теперь отображают другой контакт.

Я вручил награду Бену М., но отмечаю это как принятый ответ, так как этот код более полно проработан.

SYNStackController.h

//
//  SYNStackController.h
//  Last-in-first-out stack controller class.
//

@interface SYNStackController : NSObject {
    NSMutableArray *stack;
}

- (void) addBlock:(void (^)())block;
- (void) startNextBlock;
+ (void) performBlock:(void (^)())block;

@end

SYNStackController.m

//
//  SYNStackController.m
//  Last-in-first-out stack controller class.
//

#import "SYNStackController.h"

@implementation SYNStackController

- (id)init
{
    self = [super init];

    if (self != nil) 
    {
        stack = [[NSMutableArray alloc] init];
    }

    return self;
}

- (void)addBlock:(void (^)())block
{
    @synchronized(stack)
    {
        [stack addObject:[[block copy] autorelease]];
    }

    if (stack.count == 1) 
    {
        // If the stack was empty before this block was added, processing has ceased, so start processing.
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^{
            [self startNextBlock];
        });
    }
}

- (void)startNextBlock
{
    if (stack.count > 0)
    {
        @synchronized(stack)
        {
            id blockToPerform = [stack lastObject];
            dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^{
                [SYNStackController performBlock:[[blockToPerform copy] autorelease]];
            });

            [stack removeObject:blockToPerform];
        }

        [self startNextBlock];
    }
}

+ (void)performBlock:(void (^)())block
{
    @autoreleasepool {
        block();
    }
}

- (void)dealloc {
    [stack release];
    [super dealloc];
}

@end

В view.h, перед @interface:

@class SYNStackController;

В разделе view.h @interface:

SYNStackController *stackController;

В view.h, после раздела @interface:

@property (nonatomic, retain) SYNStackController *stackController;

В view.m, до @implementation:

#import "SYNStackController.h"

В view.m viewDidLoad:

// Initialise Stack Controller.
self.stackController = [[[SYNStackController alloc] init] autorelease];

В виде.м .:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // Set up the cell.
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    else 
    {
        // If an existing cell is being reused, reset the image to the default until it is populated.
        // Without this code, previous images are displayed against the new people during rapid scrolling.
        [cell setImage:[UIImage imageNamed:@"DefaultPicture.jpg"]];
    }

    // Set up other aspects of the cell content.
    ...

    // Store a reference to the current cell that will enable the image to be associated with the correct
    // cell, when the image subsequently loaded asynchronously. 
    objc_setAssociatedObject(cell,
                             personIndexPathAssociationKey,
                             indexPath,
                             OBJC_ASSOCIATION_RETAIN);

    // Queue a block that obtains/creates the image and then loads it into the cell.
    // The code block will be run asynchronously in a last-in-first-out queue, so that when
    // rapid scrolling finishes, the current cells being displayed will be the next to be updated.
    [self.stackController addBlock:^{
        UIImage *avatarImage = [self createAvatar]; // The code to achieve this is not implemented in this example.

        // The block will be processed on a background Grand Central Dispatch queue.
        // Therefore, ensure that this code that updates the UI will run on the main queue.
        dispatch_async(dispatch_get_main_queue(), ^{
            NSIndexPath *cellIndexPath = (NSIndexPath *)objc_getAssociatedObject(cell, personIndexPathAssociationKey);
            if ([indexPath isEqual:cellIndexPath]) {
            // Only set cell image if the cell currently being displayed is the one that actually required this image.
            // Prevents reused cells from receiving images back from rendering that were requested for that cell in a previous life.
                [cell setImage:avatarImage];
            }
        });
    }];

    return cell;
}
6 голосов
/ 27 октября 2011

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

ПРИМЕЧАНИЕ. В этом случае я использовал ARC, поэтому вам понадобится XCode 4.2 или более поздней версии. Для тех из вас, кто работает в более поздних версиях, просто измените силу, чтобы сохранить ее, и все будет в порядке, но это приведет к утечке памяти во всем, если вы этого не сделаете. добавить в релизы.

РЕДАКТИРОВАТЬ: Чтобы получить более конкретную информацию о вашем случае использования, если у вашего TableViewCell есть изображение, я бы использовал свой класс стека следующим образом, чтобы получить желаемую производительность, пожалуйста, дайте мне знать, хорошо ли она работает для вас.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    // Configure the cell...

    UIImage *avatar = [self getAvatarIfItExists]; 
    // I you have a method to check for the avatar

    if (!avatar) 
    {
        [self.blockStack addBlock:^{

            // do the heavy lifting with your creation logic    
            UIImage *avatarImage = [self createAvatar];

            dispatch_async(dispatch_get_main_queue(), ^{
                //return the created image to the main thread.
                cell.avatarImageView.image = avatarImage;
            });

        }];
    }
    else
    {
         cell.avatarImageView.image = avatar;
    }

    return cell;
}

Вот код тестирования, который показывает, что он работает как стек:

WaschyBlockStack *stack = [[WaschyBlockStack alloc] init];

for (int i = 0; i < 100; i ++)
{
    [stack addBlock:^{

        NSLog(@"Block operation %i", i);

        sleep(1);

    }];
}

Вот .h:

#import <Foundation/Foundation.h>

@interface WaschyBlockStack : NSObject
{
    NSMutableArray *_blockStackArray;
    id _currentBlock;
}

- (id)init;
- (void)addBlock:(void (^)())block;

@end

И .m:

#import "WaschyBlockStack.h"

@interface WaschyBlockStack()

@property (atomic, strong) NSMutableArray *blockStackArray;

- (void)startNextBlock;
+ (void)performBlock:(void (^)())block;

@end

@implementation WaschyBlockStack

@synthesize blockStackArray = _blockStackArray;

- (id)init
{
    self = [super init];

    if (self) 
    {
        self.blockStackArray = [NSMutableArray array];
    }

    return self;
}

- (void)addBlock:(void (^)())block
{

    @synchronized(self.blockStackArray)
    {
        [self.blockStackArray addObject:block];
    }
    if (self.blockStackArray.count == 1) 
    {
        [self startNextBlock];
    }
}

- (void)startNextBlock
{
    if (self.blockStackArray.count > 0) 
    {
        @synchronized(self.blockStackArray)
        {
            id blockToPerform = [self.blockStackArray lastObject];

            [WaschyBlockStack performSelectorInBackground:@selector(performBlock:) withObject:[blockToPerform copy]];

            [self.blockStackArray removeObject:blockToPerform];
        }

        [self startNextBlock];
    }
}

+ (void)performBlock:(void (^)())block
{
    block();
}

@end
4 голосов
/ 22 октября 2011

Простой метод, который может быть достаточно хорош для вашей задачи: используйте функцию NSOperation s зависимостей.

Когда вам нужно отправить операцию, получите операции очереди и выполните поиск самой последней отправленной (т. Е. Выполните поиск в конце массива), которая еще не была запущена. Если такой существует, установите его в зависимости от вашей новой операции с addDependency:. Затем добавьте вашу новую операцию.

Это создает цепочку обратной зависимости через незапущенные операции, которая заставляет их запускаться последовательно, по мере поступления, по мере поступления. Если вы хотите, чтобы операции n (> 1) выполнялись одновременно: найдите n -ю самую последнюю добавленную неотзапущенную операцию и добавьте к ней зависимость. (и, конечно, установите maxConcurrentOperationCount в очереди n .) Существуют крайние случаи, когда это не будет 100% LIFO, но должно быть достаточно для джаза.

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

[отредактировано, чтобы добавить:]

Я реализовал нечто очень похожее на это - таблицу пользователей, их аватары, лениво извлеченные с сайта gravatar.com на заднем плане, - и этот трюк отлично работал. Прежний код был:

[avatarQueue addOperationWithBlock:^{
  // slow code
}]; // avatarQueue is limited to 1 concurrent op

, который стал:

NSBlockOperation *fetch = [NSBlockOperation blockOperationWithBlock:^{
  // same slow code
}];
NSArray *pendingOps = [avatarQueue operations];
for (int i = pendingOps.count - 1; i >= 0; i--)
{
  NSOperation *op = [pendingOps objectAtIndex:i];
  if (![op isExecuting])
  {
    [op addDependency:fetch];
    break;
  }
}
[avatarQueue addOperation:fetch];

Значки видимо заполняются сверху вниз в первом случае. Во втором загружается верхний, затем остальные загружаются снизу вверх; а быстрая прокрутка вниз вызывает случайную загрузку, а затем немедленную загрузку (снизу) значков экрана, на котором вы останавливаетесь. Очень приятное, более "быстрое" чувство в приложении.

3 голосов
/ 27 сентября 2011

Я не пробовал этого - просто выкидывал идеи.

Вы могли бы поддерживать свой собственный стек.Добавьте в стек и поставьте в очередь в GCD в потоке переднего плана.Блок кода, который вы ставите в очередь в GCD, просто вытаскивает следующий блок из вашего стека (самому стеку потребуется внутренняя синхронизация для push & pop) и запускает его.

Другой вариант - просто пропустить работу, если естьболее n элементов в очереди.Это будет означать, что если вы быстро создадите резервную копию очереди, она быстро пробежит по очереди и только обработает

Надеюсь, что это породило некоторые идеи ... Я могу поиграть с этим позже в коде.

1 голос
/ 04 октября 2012

Я большой поклонник интерфейса и простоты использования NSOperationQueue, но мне также нужна была версия LIFO. Я закончил тем, что реализовал LIFO-версию NSOperationQueue здесь , которая довольно хорошо сохранилась для меня. Он имитирует интерфейс NSOperationQueue, но выполняет все в (приблизительно) порядке LIFO.

1 голос
/ 22 октября 2011

создать потокобезопасный стек, используя что-то вроде этого в качестве отправной точки:

@interface MONStack : NSObject <NSLocking> // << expose object's lock so you
                                           // can easily perform many pushes
                                           // at once, keeping everything current.
{
@private
    NSMutableArray * objects;
    NSRecursiveLock * lock;
}

/**
  @brief pushes @a object onto the stack.
  if you have to do many pushes at once, consider adding `addObjects:(NSArray *)`
*/
- (void)addObject:(id)object;

/** @brief removes and returns the top object from the stack */
- (id)popTopObject;

/**
  @return YES if the stack contains zero objects.
*/
- (BOOL)isEmpty;

@end

@implementation MONStack

- (id)init {
    self = [super init];
    if (0 != self) {
        objects = [NSMutableArray new];
        lock = [NSRecursiveLock new];
        if (0 == objects || 0 == lock) {
            [self release];
            return 0;
        }
    }
    return self;
}

- (void)lock
{
    [lock lock];
}

- (void)unlock
{
    [lock unlock];
}

- (void)dealloc
{
    [lock release], lock = 0;
    [objects release], objects = 0;
    [super dealloc];
}

- (void)addObject:(id)object
{
    [self lock];
    [objects addObject:object];
    [self unlock];
}

- (id)popTopObject
{
    [self lock];
    id last = 0;
    if ([objects count]) {
        last = [[[objects lastObject] retain] autorelease];
    }
    [self unlock];
    return last;
}

- (BOOL)isEmpty
{
  [self lock];
  BOOL ret = 0 == [objects count];
  [self unlock];
  return ret;
}

@end

затем используйте NSOperation подкласс (или GCD, если хотите). Вы можете разделить стек между операцией и клиентами.

так что пустой бит и основная NSOperation - несколько хитрые секции.

давайте начнем с пустого бита. это сложно, потому что он должен быть потокобезопасным:

// adding a request and creating the operation if needed:
{
    MONStack * stack = self.stack;
    [stack lock];

    BOOL wasEmptyBeforePush = [stack isEmpty];
    [stack addObject:thing];

    if (wasEmptyBeforePush) {
        [self.operationQueue addOperation:[MONOperation operationWithStack:stack]];
    }

    [stack unlock];
// ...
}

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

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

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

если задача получает выгоду от распараллеливания, то разрешить несколько задач в очереди операций.

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

1 голос
/ 22 октября 2011

Я делаю что-то вроде этого, но только для iPad, и это кажется достаточно быстрым.NSOperationQueue (или необработанный GCD) кажется самым простым подходом: все может быть автономным, и вам не нужно беспокоиться о синхронизацииКроме того, вы можете сохранить последнюю операцию и использовать setQueuePriority: для ее понижения.Затем самый последний будет извлечен из очереди первым.Или пройдите все -operations в очереди и понизьте их приоритет.(Вы, вероятно, могли бы сделать это после завершения каждого, я предполагаю, что это все равно будет значительно быстрее, чем выполнение самой работы.)

...