使用 GCD 后进先出堆栈? [英] Last In-First Out Stack with GCD?

查看:25
本文介绍了使用 GCD 后进先出堆栈?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

令我震惊的是,后进先出堆栈似乎很可能在很大程度上解决这个问题,因为每当用户停止滚动这些单元格时,下一个要更新的单元格将是当前屏幕外的其他单元格更新.Grand Central Dispatch 能做到这样的事情吗?或者不太繁重以其他方式实施?

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

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

解决方案

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

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

我已将赏金授予 Ben M. 但我将其标记为已接受的答案,因为此代码已得到更充分的处理.

SYNStackController.h

<代码>////SYNStackController.h//后进先出堆栈控制器类.//@interface SYNStackController : NSObject {NSMutableArray *stack;}- (void) addBlock:(void (^)())block;- (void) startNextBlock;+ (void) performBlock:(void (^)())block;@结尾

SYNStackController.m

<代码>////SYNStackController.m//后进先出堆栈控制器类.//#import "SYNStackController.h"@implementation SYNStackController- (id)init{self = [超级初始化];如果(自我!= nil){stack = [[NSMutableArray alloc] init];}回归自我;}- (void)addBlock:(void (^)())block{@同步(堆栈){[stack addObject:[[block copy] autorelease]];}如果(堆栈计数== 1){//如果在添加此块之前堆栈为空,则处理已停止,因此开始处理.dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);dispatch_async(队列,^{[自启动NextBlock];});}}- (void)startNextBlock{如果(堆栈计数> 0){@同步(堆栈){id blockToPerform = [stack lastObject];dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);dispatch_async(队列,^{[SYNStackController performBlock:[[blockToPerform copy] autorelease]];});[stack removeObject:blockToPerform];}[自启动NextBlock];}}+ (void)performBlock:(void (^)())block{@autoreleasepool {堵塞();}}- (void)dealloc {【堆栈释放】;[超级dealloc];}@结尾

在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 中:

//初始化堆栈控制器.self.stackController = [[[SYNStackController alloc] init] autorelease];

在 view.m 中:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {//设置单元格.静态 NSString *CellIdentifier = @"Cell";UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];如果(单元格 == 零){单元格 = [[UITableViewCell 分配] initWithStyle:UITableViewCellStyleDefault 重用标识符:CellIdentifier];}别的{//如果正在重用现有单元格,则将图像重置为默认值,直到填充为止.//如果没有此代码,则在快速滚动过程中,之前的图像会与新人一起显示.[cell setImage:[UIImage imageNamed:@"DefaultPicture.jpg"]];}//设置单元格内容的其他方面....//存储对当前单元格的引用,这将使图像与正确的关联//单元格,当图像随后异步加载时.objc_setAssociatedObject(单元格,personIndexPathAssociationKey,索引路径,OBJC_ASSOCIATION_RETAIN);//将获取/创建图像的块排队,然后将其加载到单元格中.//代码块将在后进先出队列中异步运行,以便当//快速滚动完成,当前显示的单元格将是下一个要更新的单元格.[self.stackController addBlock:^{UIImage *avatarImage = [self createAvatar];//实现此目的的代码在此示例中未实现.//该块将在后台 Grand Central Dispatch 队列中处理.//因此,请确保此更新 UI 的代码将在主队列上运行.dispatch_async(dispatch_get_main_queue(),^{NSIndexPath *cellIndexPath = (NSIndexPath *)objc_getAssociatedObject(cell, personIndexPathAssociationKey);if ([indexPath isEqual:cellIndexPath]) {//如果当前显示的单元格是实际需要此图像的单元格,则仅设置单元格图像.//防止重用的单元格从渲染中接收返回的图像,这些图像是在前一个生命周期中为该单元格请求的.[cell setImage:avatarImage];}});}];返回单元格;}

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 where there isn't one they are an avatar rendered based on stored data. I presently have these images being updated on a background thread using GCD. However, this loads the images in the order they were requested, which means during rapid scrolling the queue becomes lengthy and when the user stops scrolling the current cells are the last to get updated. On the iPhone 4, the problem isn't really noticeable, but I am keen to support older hardware and am testing on an iPhone 3G. The delay is tolerable but quite noticeable.

It strikes me that a Last In-First Out stack would seem likely to largely resolve this issue, as whenever the user stopped scrolling those cells would be the next to be updated and then the others that are currently off-screen would be updated. Is such a thing possible with Grand Central Dispatch? Or not too onerous to implement some other way?

Note, by the way, that I am using Core Data with a SQLite store and I am not using an NSFetchedResultsController because of a many-to-many relationship that has to be traversed in order to load the data for this view. (As far as I am aware, that precludes using an NSFetchedResultsController.) [I've discovered an NSFetchedResultsController can be used with many-to-many relationships, despite what the official documentation appears to say. But I'm not using one in this context, yet.]

Addition: Just to note that while the topic is "How do I create a Last In-First Out Stack with GCD", in reality I just want to solve the issue outlined above and there may be a better way to do it. I am more than open to suggestions like timthetoolman's one that solves the problem outlined in another way; if such a suggestion is finally what I use I'll recognize both the best answer to the original question as well as the best solution I ended up implementing... :)

解决方案

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

这篇关于使用 GCD 后进先出堆栈?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆