使用GCD进行最后进入堆叠?

时间:2011-09-27 10:43:03

标签: iphone objective-c ios uitableview grand-central-dispatch

我有一个UITableView,显示与每行中的联系人关联的图像。在一些情况下,这些图像在地址簿联系人图像的第一显示器上被读取,并且在没有一个的情况下,他们是基于存储的数据呈现的化身。我现在使用GCD在后台线程上更新这些图像。但是,这会按照请求的顺序加载图像,这意味着在快速滚动期间队列变得冗长,当用户停止滚动时,当前单元格是 last 以进行更新。在iPhone 4上,这个问题并不是很明显,但我很想支持旧硬件并在iPhone 3G上进行测试。延迟是可以容忍的,但非常明显。

让我觉得Last In-First Out堆栈似乎很可能在很大程度上解决了这个问题,因为每当用户停止滚动这些单元格时,下一个将被更新,然后其他当前屏幕外的将是更新。 Grand Central Dispatch可以做到这样吗?或者不太费力地实施其他方式?

顺便说一句,请注意,我正在使用带有SQLite存储的Core Data而我没有使用NSFetchedResultsController ,因为为了加载数据而必须遍历多对多关系这个观点。 (据我所知,这排除了使用NSFetchedResultsController。) [我发现NSFetchedResultsController可以与多对多关系一起使用,尽管官方文档似乎在说。但是我在这种情况下并没有使用它。]

添加:请注意,虽然主题是“我如何使用GCD创建最后进入第一个堆栈”,但实际上我只想解决上面列出的问题,可能做一个更好的方法。我非常愿意接受像timthetoolman那样以另一种方式解决问题的建议;如果这样的建议最终是我使用的,我会认识到原始问题的最佳答案以及我最终实施的最佳解决方案......:)

8 个答案:

答案 0 :(得分:16)

由于设备的内存限制,您应该根据需要和后台GCD队列加载图像。在cellForRowAtIndexPath:方法中,检查您的联系人的图像是否为nil或已被缓存。如果图像在缓存中为零或不存在,请使用嵌套的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会议视频“Introducing Blocks和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。

祝你好运。

答案 1 :(得分:11)

下面的代码创建了一个灵活的后进先出堆栈,使用Grand Central Dispatch在后台处理。 SYNStackController类是通用的并且可重用,但是此示例还提供了问题中标识的用例的代码,异步呈现表格单元格图像,并确保在快速滚动停止时,当前显示的单元格是下一个要更新的单元格。“ p>

感谢Ben M.对这个问题的回答提供了这个问题的初始代码。 (他的回答还提供了可用于测试堆栈的代码。)此处提供的实现不需要ARC,仅使用Grand Central Dispatch而不是performSelectorInBackground。下面的代码还使用objc_setAssociatedObject存储对当前单元格的引用,当随后异步加载图像时,该objc_setAssociatedObject将使渲染图像与正确的单元格相关联。如果没有此代码,为以前的联系人呈现的图像将被错误地插入到重用的单元格中,即使它们现在显示的是不同的联系人。

我已经将这笔奖金授予Ben M.但是我将此标记为已接受的答案,因为此代码已经完全通过。

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];

在view.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 :(得分:6)

好的,我已经测试了这个并且它有效。该对象只是从堆栈中拉出下一个块并异步执行它。它目前只适用于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

答案 3 :(得分:4)

对您的任务来说可能足够简单的简单方法:使用NSOperation s'依赖项功能。

当您需要提交操作时,获取队列的操作并搜索尚未启动的最近提交的操作(即从阵列末尾搜索)。如果存在这样的一个,请将其设置为依赖于addDependency:的新操作。然后添加新操作。

这将通过非启动操作构建反向依赖关系链,这将强制它们以可用的顺序串行运行,后进先出。如果要允许 n (&gt; 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];

在前一种情况下,图标从上到下明显地填充。在第二个中,顶部装载,然后从底部向上装载其余部分;并且快速向下滚动会导致偶尔加载,然后立即加载(从底部)你停下来的屏幕图标。非常光滑,对应用程序有更“快捷”的感觉。

答案 4 :(得分:3)

我没有试过这个 - 只是把想法扔出去。

您可以维护自己的堆栈。添加到堆栈并在前台线程上排队到GCD。你排队到GCD的代码块只是将你的堆栈中的下一个块拉出来(堆栈本身需要push&amp; pop的内部同步)并运行它。

如果队列中有超过n个项目,则另一个选项可能是简单地跳过工作。这意味着如果您快速备份队列,它将快速按下队列并仅处理&lt; ñ。如果向上滚动,单元格重用队列将获得另一个单元格,然后您将再次排队以加载图像。这将始终优先排序最近排队的n。我不确定的是排队的块如何知道队列中的项目数。也许有一种GCD方式可以实现这一目标?如果没有,你可以有一个线程安全计数器来递增/递减。排队时递增,处理时递减。如果你这样做,我会增加和减少作为双方的第一行代码。

希望能引发一些想法......我可以稍后在代码中用它来播放它。

答案 5 :(得分:1)

我这样做,但仅限iPad,而且看起来足够快。 NSOperationQueue(或原始GCD)似乎是最简单的方法,因为一切都可以是自包含的,您不需要担心同步。此外,您可以保存上一个操作,并使用setQueuePriority:来降低它。然后,最新的一个将首先从队列中拉出。或者浏览队列中的所有-operations并降低其优先级。 (你可以在完成每一个之后做到这一点,我认为这仍然比工作本身快得多。)

答案 6 :(得分:1)

创建一个线程安全堆栈,使用这样的东西作为起点:

@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。客户端将在需要时创建新操作。

支持取消较慢的请求(例如网络或磁盘)可能会产生巨大的差异。在耗尽队列的操作的情况下取消将要求请求视图在其出列时可以移除其请求(例如,在滚动期间重复使用)。

另一个常见的缺陷:图像的立即异步加载(例如,将操作添加到操作队列)可能容易降低性能。测量

如果任务受益于并行化,则允许在操作队列中执行多个任务。

如果您的程序能够生成冗余请求(假设用户双向滚动),您还应该识别它们。

答案 7 :(得分:1)

我非常喜欢NSOperationQueue的界面和易用性,但我还需要一个LIFO版本。我最终实现了NSOperationQueue here的LIFO版本,这对我来说非常好。它模仿NSOperationQueue的接口,但以(大致)LIFO顺序执行。