最后先进出栈与GCD?
我有一个UITableView显示图像与每一行中的联系人。 在某些情况下,这些图像在地址簿联系人图像的第一个显示中被读取,并且在没有一个的情况下,这些图像是基于存储的数据呈现的化身。 我目前正在使用GCD在后台线程上更新这些图像。 但是,这会按照请求的顺序加载图像,这意味着在快速滚动队列过程中会变得冗长,并且当用户停止滚动时,当前单元格将被更新。 在iPhone 4上,问题不是很明显,但我热衷于支持旧的硬件,并在iPhone 3G上进行testing。 延迟是可以忍受的,但相当明显。
这让我觉得后进先出栈似乎可以很大程度上解决这个问题,因为每当用户停止滚动这些单元格将是下一个被更新,然后当前在屏幕外的其他将被更新。 Grand Central Dispatch有这种可能吗? 或者执行一些其他方式不是太繁重?
请注意,顺便说一下,我正在使用核心数据与SQLite存储,我没有使用NSFetchedResultsController 由于多对多的关系,必须遍历为了加载这个视图的数据。 (据我所知,这排除了使用NSFetchedResultsController。) [我发现一个NSFetchedResultsController可以使用多对多的关系,尽pipe官方文档似乎说。 但是我在这方面还没有用到。]
补充:只是要注意,虽然话题是“如何用GCD创build最后的先入栈”,但实际上我只是想解决上面提到的问题,可能有更好的办法。 我更愿意接受像蒂蒙托曼那样的解决以另一种方式概括的问题的build议。 如果这样的build议终于是我使用的,我会认识到最初的问题的最佳答案,以及我最终实现的最佳解决scheme… 🙂
由于设备的内存限制,您应该按需将图像加载到后台GCD队列中。 在cellForRowAtIndexPath:方法检查,看你的联系人的图像是零或已被caching。 如果图像为零或不在caching中,则使用嵌套的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会议video“介绍块和Grand Central调度”显示了一个使用嵌套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方法渲染的图像以更高的优先级渲染。 所以,由于队列的优先级不同,你会有一个“穷人”的后进先出。
祝你好运。
下面的代码创build了一个灵活的后进先出堆栈,在后台使用Grand Central Dispatch进行处理。 SYNStackController类是通用且可重用的,但是此示例还提供了问题中标识的用例的代码,asynchronous渲染表格单元格图像,并确保快速滚动停止时,当前显示的单元格将被更新。
对Ben M.的赞扬,对这个问题的回答提供了这个基础的最初的代码。 (他的回答也提供了可用于testing堆栈的代码)。这里提供的实现不需要ARC,只使用Grand Central Dispatch而不是执行SelectorInBackground。 下面的代码还使用objc_setAssociatedObject存储对当前单元格的引用,这将使渲染的图像与正确的单元格相关联,以便随后加载asynchronous图像。 如果没有此代码,为以前的联系人呈现的图像将错误地插入到重用的单元格中,即使它们现在正在显示不同的联系人。
我已经把赏金给了本·米,但是我把这个作为被接受的答案,因为这个代码更全面。
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
在@interface之前的view.h中:
@class SYNStackController;
在view.h @interface部分中:
SYNStackController *stackController;
在@interface部分之后的view.h中:
@property (nonatomic, retain) SYNStackController *stackController;
在@implementation之前的view.m中:
#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; }
好吧,我已经testing了这个,它工作。 该对象只是将下一个块从堆栈中取出并asynchronous执行。 它目前只适用于无效返回块,但你可以做一些奇特的事情,像添加一个对象,它将有一个块和一个委托来传递块的返回types。
注意:我在这里使用了ARC,所以你需要XCode 4.2或者更高版本,对于那些在以后版本中的用户,只要改变strong来保留,你应该没问题,但是如果你不添加在发布。
编辑:要更具体到你的用例,如果你的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; }
以下是testing代码,显示它作为一个堆栈工作:
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
还有他们:
#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
一个简单的方法,可以很好你的任务足够:使用NSOperation
的依赖function。
当你需要提交一个操作时,获取队列的操作,然后search最近提交的那个(也就是从数组末尾search)还没有开始的操作。 如果存在这样的一个,请将其设置为依赖于您的新操作addDependency:
然后添加您的新操作。
这将通过非启动操作构build反向依赖关系链,这将强制它们按照后进先出的方式运行。 如果要允许n (> 1)个操作同时运行:find最近添加的第n个未启动的操作并向其添加依赖关系。 (当然也可以将队列的maxConcurrentOperationCount
设置为n) 。有些情况下,这不会是100%LIFO,但对爵士应该足够好。
(这不包括重新排列优先级的操作,例如,如果用户向下滚动列表,然后备份一点点,所有的快于队列都可以填充图像。如果你想解决这个问题,并给自己一种find相应的已经排队但未启动的操作的方法,可以清除对该操作的依赖关系,这有效地将其返回到“行首”,但是由于纯先进先出已经足够好了,你可能不需要这个幻想。)
[编辑补充:]
我已经实现了一些非常像这样的东西 – 一张用户桌面,他们在后台从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];
在前一种情况下,图标显然是从上到下填充的。 在第二个,第一个加载,然后从下往上的其余加载; 快速滚动会导致偶尔加载,然后立即加载(从底部)的屏幕图标,你停下来。 非常漂亮,对应用程序感觉更“快捷”。
我还没有尝试过 – 只是把想法扔出去。
你可以维护自己的堆栈。 添加到堆栈并在前台线程上排队到GCD。 您排队到GCD的代码块只是将下一个块从堆栈中拉出(堆栈本身需要内部同步用于push&pop)并运行它。
如果队列中有多于n个项目,另一个select可能是简单地跳过工作。 这意味着如果你很快得到队列的备份,它将很快按下队列,只处理<n。 如果向后滚动,单元重用队列将获得另一个单元,然后再次将其排队以加载图像。 这总是优先排列最近排队的。 我不确定的事情是排队块如何知道队列中的项目数量。 也许有一个GCD的方式来解决这个问题? 如果没有,你可以有一个线程安全计数器来递增/递减。 排队时递增,递减。 如果你这样做,我会增加和减less作为双方的第一行代码。
希望引发了一些想法…我可能会稍后在代码中使用它。
我做这样的事情,但只有iPad,似乎足够快。 NSOperationQueue
(或原始GCD)似乎是最简单的方法,因为一切都可以自成一体,您不必担心同步。 此外,您可能能够保存上一个操作,并使用setQueuePriority:
来降低它。 那么最近的一个会先从队列中拉出来。 或者经过队列中的所有-operations
并降低其优先级。 (完成每一个后,你可能会这样做,我认为这样做仍然会比做这个工作快得多)。
创build一个线程安全堆栈,使用这样的事情作为一个起点:
@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主要应该只是耗尽堆栈,为每个任务创build一个自动释放池,并检查取消。 当堆栈清空或操作被取消时,清理并退出main。 客户将在需要时创build一个新的操作。
支持取消较慢的请求(如networking或磁盘)可以产生巨大的差异。 在用尽队列的操作的情况下的取消将要求请求视图可以在其出队(例如,在滚动期间被重用)时移除其请求。
另一个常见的错误是:即时asynchronous加载(例如,将操作添加到操作队列中)可能会轻易降低性能。 测量。
如果任务受益于并行化,则允许在操作队列中执行多个任务。
如果您的程序能够生成这些请求,则还应该在您的任务队列中标识多余的请求(想象一下双向滚动的用户)。
我是NSOperationQueue
的界面和易用性的忠实粉丝,但我也需要一个LIFO版本。 我最终实现了一个NSOperationQueue
的LIFO版本, NSOperationQueue
版本对我来说挺好的。 它模仿NSOperationQueue
的接口,但以(大致)LIFO顺序执行。