Подтвердить что ты не робот

Последний в первый раз стек с GCD?

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

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

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

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

4b9b3361

Ответ 1

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

Kudos to Ben M., ответ на который поставил исходный код, на котором он был основан. (Его ответ также дает код, который вы можете использовать для проверки стека.) Реализация, предусмотренная здесь, не требует ARC и использует исключительно Grand Central Dispatch, а не выполняет функциюSelectorInBackground. В приведенном ниже коде также хранится ссылка на текущую ячейку с использованием объекта objc_setAssociatedObject, который позволит связать визуализированное изображение с правильной ячейкой, когда изображение будет загружено асинхронно. Без этого кода изображения, отображаемые для предыдущих контактов, будут неправильно вставлены в повторно используемые ячейки, даже если они теперь отображают другой контакт.

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

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;

В представлении .m, перед @implementation:

#import "SYNStackController.h"

В view.m viewDidLoad:

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

В представлении .m:

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

Ответ 2

Из-за ограничений памяти устройства вы должны загружать изображения по требованию и на фоне очереди 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 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 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 "бедных мужчин".

Удачи.

Ответ 3

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

ПРИМЕЧАНИЕ. Я использовал 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

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

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

Это создает цепочку обратной зависимости через незапущенные операции, которая заставит их запускать последовательно, последний раз в первый раз, как доступно. Если вы хотите разрешить одновременное выполнение операций n ( > 1): найдите последнюю недавно добавленную нераспределенную операцию и добавьте к ней зависимость. (и, конечно, установите очередь 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];

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

Ответ 5

Я не пробовал это - просто бросая идеи там.

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

Другим вариантом может быть просто пропустить работу, если в очереди больше, чем n элементов. Это означало бы, что если вы быстро получите резервную копию очереди, она быстро перейдет в очередь и только процесс < п. Если вы прокрутите резервную копию, очередь повторного использования ячеек, получит другую ячейку, а затем вы снова поставите ее в очередь, чтобы загрузить изображение. Это всегда ставило бы приоритет в последнее время в очереди. Вещь, о которой я не уверен, заключается в том, как блок в очереди будет знать о количестве элементов в очереди. Может быть, есть способ GCD? Если нет, у вас может быть счетчик потокобезопасности для увеличения/уменьшения. Приращение при очередности, уменьшение на обработку. Если вы это сделаете, я буду увеличивать и уменьшать как первую строку кода с обеих сторон.

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

Ответ 6

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

Ответ 7

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

@interface MONStack : NSObject <NSLocking> // << expose object 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 должен просто пройти и выгрузить стек, создать пул автозапуска для каждой задачи и проверить отмену. когда стопка пуста или операция отменена, очистка и выход из основной. при необходимости клиент создаст новую операцию.

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

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

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

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

Ответ 8

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