下面的代码创建了一个灵活的后进先出堆栈,该堆栈使用 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;
}