【问题标题】:Last In-First Out Stack with GCD?使用 GCD 的后进先出堆栈?
【发布时间】:2011-09-27 10:43:03
【问题描述】:

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

让我感到震惊的是,后进先出堆栈似乎很可能在很大程度上解决了这个问题,因为每当用户停止滚动时,这些单元格将是下一个要更新的单元格,然后其他当前不在屏幕上的单元格将是更新。 Grand Central Dispatch 有可能做到这一点吗?还是不太繁重而无法以其他方式实现?

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

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

【问题讨论】:

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


    【解决方案1】:

    由于设备的内存限制,您应该在后台 GCD 队列中按需加载图像。在 cellForRowAtIndexPath: 方法中检查联系人的图像是否为 nil 或已被缓存。如果图像为 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 and 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。

    祝你好运。

    【讨论】:

    • 有趣。第二个代码块的大部分内容正是我已经拥有的——我只是错过了查看视图当前是否正在滚动的检查。在某些方面,我仍然更喜欢后进先出的方法,因为我非常希望在空闲周期中在后台缓存其他图像一旦加载可见图像。但是,您提出的解决方案具有优雅的简单性,与此相比,后进先出解决方案可能是不必要的优化/过度工程。当我回到可以处理这段代码的机器上时(几天),我期待着对此进行测试。
    • 举个例子,这似乎很可靠。我仍然会在 dispatch_async 块的开头检查是否需要图像。这将有助于提高快速滚动的效率。
    • 在缓慢而有意的滚动过程中不会显示内容失败吗?
    • 可以进一步优化。例如,可以将获取图像的 dispatch_async 放入 if then 语句的第一部分。答案的重点是,对于移动设备,应该在后台队列中按需提供图像,而不是尝试加载所有图像。
    • 在考虑了David的评论后,我对代码进行了编辑和优化。
    【解决方案2】:

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

    Ben M. 致敬,他对这个问题的回答提供了此问题所基于的初始代码。 (他的回答还提供了可用于测试堆栈的代码。)此处提供的实现不需要 ARC,并且仅使用 Grand Central Dispatch 而不是 performSelectorInBackground。下面的代码还使用 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;
    }
    

    【讨论】:

    • 嘿,邓肯,感谢您的赏金。真高兴你做到了。我选择了 performselector,因为我在使用 dispatch_async 时遇到了错误的访问异常。我没有想到要复制该块两次,这可能是它为您工作的原因。这是一个很酷的问题!
    • 有人能解释一下 personIndexPathAssociationKey 应该是什么吗?我不明白这部分解决方案。
    • 嘿 Slee,推荐的方法是在 view.m 的顶部添加类似 static char personIndexPathAssociationKey; 的内容。我与 Duncan 不同的做法是在它前面加上 & 以使用它的地址。这就是 Apple 在its example 中所做的。
    【解决方案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
    

    【讨论】:

    • 这似乎是最有前途的方法,并打算根据要求实施 LIFO 堆栈。但是,当我实现此代码时,我发现无论是对于我的代码还是对于您的示例代码,它都不像堆栈那样运行,因为 self.blockStackArray.count 永远不会超过 1 — 每个块都在下一个被添加到数组中,这意味着它们按照接收到的顺序执行,先进先出。显然有些东西不在这里。你能告诉我我可能错过了什么吗?
    • 我已将赏金奖励给这个问题,因为这是最接近提供真正 LIFO 堆栈的完全有效的答案。但是,存在上述实现问题,这意味着所概述的代码实际上似乎并未提供 LIFO 功能。我希望很快解决这些问题,并将发布修改后的代码,并附上适当的属性,作为未来读者可接受的答案。 :)
    • 感谢您为此所做的工作。请参阅我的最终答案,在您的代码上进行开发。我上面概述的问题可能与将示例改装到非 ARC 环境有关,所以我不能 100% 确定在 ARC 环境中仍然需要我对代码所做的多少更改。 . 虽然这对于任何使用基于 ARC 的代码的人来说都很容易确定。干杯!
    【解决方案4】:

    一个简单的方法可能对您的任务足够好:使用NSOperations 的依赖功能。

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

    这会通过未启动的操作构建反向依赖链,这将迫使它们以可用的后进先出顺序运行。如果您想允许 n (> 1) 个操作同时运行:找到第 n 个最近添加的未启动操作并将依赖项添加到它。 (当然,将队列的 maxConcurrentOperationCount 设置为 n。)在某些极端情况下,这不会是 100% LIFO,但对于爵士乐来说应该足够了。

    (如果(例如)用户向下滚动列表然后后退一点,这不包括重新确定操作的优先级,所有这些都比队列填充图像的速度要快。如果您想解决这种情况,并且已经给了自己一种方法来定位相应的已经入队但未开始的操作,您可以清除对该操作的依赖关系。这有效地将其撞回“行首”。但由于纯粹的 first-in- first-out 几乎已经足够好了,你可能不需要这么花哨。)

    [编辑添加:]

    我已经实现了非常类似的东西 - 一张用户表,他们的头像在后台从 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];
    

    在前一种情况下,图标明显地从上到下填充。第二个,上一个加载,然后从下向上加载其余的;并且快速向下滚动会导致偶尔加载,然后立即加载(从底部)您停止的屏幕图标。该应用程序非常流畅,更加“活泼”。

    【讨论】:

    • 该死的。这是聪明的想法。在这个线程中有很多好主意。我认为依赖关系是相当有效地实现的,因此如果有 100 个队列,则在处理下一个要处理的项目之前不会花费不合理的时间来处理 99 个依赖项,然后它就可以下次不必遍历 98 个依赖项,下一次遍历 97 个依赖项,以此类推,就可以有效地回退该列表?有兴趣测试这种方法... :)
    【解决方案5】:

    我没有尝试过 - 只是把想法扔在那里。

    您可以维护自己的堆栈。添加到堆栈并在前台线程上排队到 GCD。您排队到 GCD 的代码块只是将下一个代码块从堆栈中拉出(堆栈本身需要内部同步才能推送和弹出)并运行它。

    如果队列中的项目超过 n 个,另一种选择可能是简单地跳过工作。这意味着如果您快速备份队列,它会快速通过队列并且只处理

    希望这能激发一些想法……我以后可能会在代码中使用它。

    【讨论】:

    • 这引发了一些想法。我一直想知道的一件事是 GCD 是否支持某种方式来将当前在队列中的作业标记为不再需要。这可能会提供一个机会来实现类似于您建议的跳过排长队的工作。
    • 堆栈的内部同步可以通过一个特定的 GCD 队列来完成,所有读取器/写入器都使用 dispatch_sync。
    • 如果您使用 NSOperation,可以通过 -cancel 标记它们。
    【解决方案6】:

    我做了类似的事情,但仅限 iPad,而且速度似乎足够快。 NSOperationQueue(或原始 GCD)似乎是最简单的方法,因为一切都可以自包含,您无需担心同步。此外,您也许可以保存最后一次操作,并使用setQueuePriority: 降低它。然后将首先从队列中拉出最近的一个。或者通过队列中的所有-operations 并降低它们的优先级。 (您可能在完成每一项后都可以这样做,我认为这仍然比完成工作本身要快得多。)

    【讨论】:

      【解决方案7】:

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

      @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 main 是比较棘手的部分。

      让我们从空位开始。这很棘手,因为它需要是线程安全的:

      // 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 应该只是遍历并耗尽堆栈,为每个任务创建一个自动释放池,并检查取消。当堆栈为空或操作被取消时,清理并退出main。客户端将在需要时创建新操作。

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

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

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

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

      【讨论】:

        【解决方案8】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-10-04
          • 2021-03-19
          • 2011-11-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-01-14
          相关资源
          最近更新 更多