Last In-First Out Stack with GCD?

前端 未结 8 1405
予麋鹿
予麋鹿 2020-11-28 22:01

I have a UITableView that displays images associated with contacts in each row. In some cases these images are read on first display from the address book contact image, and

相关标签:
8条回答
  • 2020-11-28 22:21

    The code below creates a flexible last in-first out stack that is processed in the background using Grand Central Dispatch. The SYNStackController class is generic and reusable but this example also provides the code for the use case identified in the question, rendering table cell images asynchronously, and ensuring that when rapid scrolling stops, the currently displayed cells are the next to be updated.

    Kudos to Ben M. whose answer to this question provided the initial code on which this was based. (His answer also provides code you can use to test the stack.) The implementation provided here does not require ARC, and uses solely Grand Central Dispatch rather than performSelectorInBackground. The code below also stores a reference to the current cell using objc_setAssociatedObject that will enable the rendered image to be associated with the correct cell, when the image is subsequently loaded asynchronously. Without this code, images rendered for previous contacts will incorrectly be inserted into reused cells even though they are now displaying a different contact.

    I've awarded the bounty to Ben M. but am marking this as the accepted answer as this code is more fully worked through.

    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
    

    In the view.h, before @interface:

    @class SYNStackController;
    

    In the view.h @interface section:

    SYNStackController *stackController;
    

    In the view.h, after the @interface section:

    @property (nonatomic, retain) SYNStackController *stackController;
    

    In the view.m, before @implementation:

    #import "SYNStackController.h"
    

    In the view.m viewDidLoad:

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

    In the 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;
    }
    
    0 讨论(0)
  • 2020-11-28 22:23

    create a thread safe stack, using something like this as a starting point:

    @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
    

    then use an NSOperation subclass (or GCD, if you prefer). you can share the stack between the operation and the clients.

    so the empty bit and the NSOperation main are the somewhat tricky sections.

    let's start with the empty bit. this is tricky because it needs to be threadsafe:

    // 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];
    // ...
    }
    

    the NSOperation main should just go through and exhaust the stack, creating an autorelease pool for each task, and checking for cancellation. when the stack is empty or the operation is cancelled, cleanup and exit main. the client will create a new operation when needed.

    supporting cancellation for slower requests (e.g. network or disk) can make a huge difference. cancellation in the case of the operation which exhausted the queue would require that the requesting view could remove its request when it is dequeued (e.g. for reuse during scrolling).

    another common pitfall: immediate async loading (e.g. adding the operation to the operation queue) of the image may easily degrade performance. measure.

    if the task benefits from parallelization, then allow multiple tasks in the operation queue.

    you should also identify redundant requests (imagine a user scrolling bidirectionally) in your task queue, if your program is capable of producing them.

    0 讨论(0)
  • 2020-11-28 22:31

    Ok, I've tested this and it works. The object just pulls the next block off the stack and executes it asynchronously. It currently only works with void return blocks, but you could do something fancy like add an object that will has a block and a delegate to pass the block's return type back to.

    NOTE: I used ARC in this so you'll need the XCode 4.2 or greater, for those of you on later versions, just change the strong to retain and you should be fine, but it will memory leak everything if you don't add in releases.

    EDIT: To get more specific to your use case, if your TableViewCell has an image I would use my stack class in the following way to get the performance you want, please let me know if it work well for you.

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

    Here's the testing code that show's that it works as a stack:

    WaschyBlockStack *stack = [[WaschyBlockStack alloc] init];
    
    for (int i = 0; i < 100; i ++)
    {
        [stack addBlock:^{
    
            NSLog(@"Block operation %i", i);
    
            sleep(1);
    
        }];
    }
    

    Here's the .h:

    #import <Foundation/Foundation.h>
    
    @interface WaschyBlockStack : NSObject
    {
        NSMutableArray *_blockStackArray;
        id _currentBlock;
    }
    
    - (id)init;
    - (void)addBlock:(void (^)())block;
    
    @end
    

    And the .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
    
    0 讨论(0)
  • 2020-11-28 22:36

    I haven't tried this - just throwing ideas out there.

    You could maintain your own stack. Add to the stack and queue to GCD on the foreground thread. The block of code you queue to GCD simply pulls the next block off your stack (the stack itself would need internal synchronization for push & pop) and runs it.

    Another option may be to simply skip the work if there's more than n items in the queue. That would mean that if you quickly got the queue backed up, it would quickly press through the queue and only process < n. If you scroll back up, the cell reuse queue, would get another cell and then you would queue it again to load the image. That would always prioritize the n most recently queued. The thing I'm not sure about is how the queued block would know about the number of items in the queue. Perhaps there's a GCD way to get at that? If not, you could have a threadsafe counter to increment/decrement. Increment when queueing, decrement on processing. If you do that, I would increment and decrement as the first line of code on both sides.

    Hope that sparked some ideas ... I may play it around with it later in code.

    0 讨论(0)
  • 2020-11-28 22:37

    Because of the memory constraints of the device, you should load the images on demand and on a background GCD queue. In the cellForRowAtIndexPath: method check to see if your contact's image is nil or has been cached. If the image is nil or not in cache, use a nested dispatch_async to load the image from the database and update the tableView cell.

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

    The WWDC2010 conference video "Introducing Blocks and Grand Central Dispatch" shows an example using the nested dispatch_async as well.

    another potential optimization could be to start downloading the images on a low priority background queue when the app launches. i.e.

     // 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];
                   }
    
            });
     });
    

    With this nested dispatch, we are rendering the images on an extremely low priority queue. Putting the image rendering on the background priority queue will allow the images being rendered from the cellForRowAtIndexPath method above to be rendered at a higher priority. So, because of the difference in priorities of the queues, you will have a "poor mans" LIFO.

    Good luck.

    0 讨论(0)
  • 2020-11-28 22:41

    A simple method that may be Good Enough for your task: use NSOperations' dependencies feature.

    When you need to submit an operation, get the queue's operations and search for the most recently submitted one (ie. search back from the end of the array) that hasn't been started yet. If such a one exists, set it to depend on your new operation with addDependency:. Then add your new operation.

    This builds a reverse dependency chain through the non-started operations that will force them to run serially, last-in-first-out, as available. If you want to allow n (> 1) operations to run simultaneously: find the n th most recently added unstarted operation and add the dependency to it. (and of course set the queue's maxConcurrentOperationCount to n.) There are edge cases where this won't be 100% LIFO but it should be good enough for jazz.

    (This doesn't cover re-prioritizing operations if (e.g.) a user scrolls down the list and then back up a bit, all faster than the queue can fill in the images. If you want to tackle this case, and have given yourself a way to locate the corresponding already-enqueued-but-not-started operation, you can clear the dependencies on that operation. This effectively bumps it back to the "head of the line". But since pure first-in-first-out is almost good enough already, you may not need to get this fancy.)

    [edited to add:]

    I've implemented something very like this - a table of users, their avatars lazy-fetched from gravatar.com in the background - and this trick worked great. The former code was:

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

    which became:

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

    The icons visibly populate from the top down in the former case. In the second, the top one loads, then the rest load from the bottom up; and scrolling rapidly down causes occasional loading, then immediate loading (from the bottom) of icons of the screenful you stop at. Very slick, much "snappier" feel to the app.

    0 讨论(0)
提交回复
热议问题