I am loading an image to a table view cell, each cell has an image. I've adapter a couple tutorials to the code below, but I am still having slow down.
I am loading these images from the documents directory. Any tips or ideas on how to speed this process up?
Edit Revised Code:
Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;
// did we already cache a copy of the image?
if (beer.image != nil) {
// good. use it. this will run quick and this will run most of the time
cell.beerImage.image = beer.image;
} else {
// it must be the first time we've scrolled by this beer. do the expensive
// image init off the main thread
cell.beerImage.image = nil; // set a default value here. nil is good enough for now
[self loadImageForBeer:beer atIndexPath:indexPath];
}
- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
beer.image = image;
dispatch_sync(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.beerImage.image = image;
});
});
}
Your algorithm looks pretty good. You've avoided many of the typical pitfalls. If you're still having UI performance problems, I'd suggest a couple of things:
You should try caching your images in memory. You could use NSMutableArray or NSMutableDictionary, but at Best way to cache images on ios app? Caleb discusses the merits of the NSCache class, which simplifies the process. If you do cache images, make sure you respond to memory pressure and purge the cache if necessary. You can respond to didReceiveMemoryWarning or add yourself as an observer to the notification center's UIApplicationDidReceiveMemoryWarningNotification.
Make sure your cached images are thumbnail sized or else you'll always have a little stuttering in your UI (if you need a resizing algorithm, let us know) and it will use up memory unnecessarily;
When you dispatch your image update back to the main queue, you should do so asynchronously (why have that background queue hang around and tie up resources as it waits for the the block to be sent back to the main queue to finish ... this is especially an issue once you have a couple of images backed up during a fast scroll); and
When you dispatch back to the main queue, you should check to make sure cell you get from cellForRowAtIndexPath is not nil (because if cell loading logic gets too backed up (esp on slower devices), you could theoretically have the cell in question having scrolled off the screen and your algorithm could crash).
I use an algorithm very much like yours, with almost the same GCD structure (with the above caveats) and it's pretty smooth scrolling, even on older devices. If you want me to post code, I'm happy to.
If you're still having troubles, the CPU profiler is pretty great for identifying the bottlenecks and letting you know where you should focus your attention. There are some great WWDC sessions available online which focus on how to use Instruments to identify performance bottlenecks, and I found them to be very helpful to gain proficiency with Instruments.
Here is my code. In viewDidLoad, I initialize my image cache:
- (void)initializeCache
{
self.imageCache = [[NSCache alloc] init];
self.imageCache.name = #"Custom Image Cache";
self.imageCache.countLimit = 50;
}
And then I use this in my tableView:cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ilvcCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// set the various cell properties
// now update the cell image
NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved
UIImage *image = [self.imageCache objectForKey:imagename];
if (image)
{
// if we have an cachedImage sitting in memory already, then use it
cell.imageView.image = image;
}
else
{
cell.imageView.image = [UIView imageNamed:#"blank_image.png"];
// the get the image in the background
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// get the UIImage
UIImage *image = [self getImage:imagename];
// if we found it, then update UI
if (image)
{
dispatch_async(dispatch_get_main_queue(), ^{
// if the cell is visible, then set the image
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
cell.imageView.image = image;
[self.imageCache setObject:image forKey:imagename];
});
}
});
}
return cell;
}
and
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
[self.imageCache removeAllObjects];
}
As an aside, one further optimization that you might contemplate would be to preload your cached images in a separate queue, rather than loading images in a separate thread just-in-time. I don't think it's necessary, as this seems to be more than fast enough for me, but it's one more option to speed up the UI.
Not much you can do here for the initial load, you're about as fast as it gets.
If it's still too slow, try loading smaller images if you can.
A couple of things:
First, be careful with -imageWithContentsOfFile, it won't cache anything. You're taking the full hit every time you load the image, as opposed to -imageNamed that'll keep the image warm in some cache.
You can of course cache that in your domain object, but I'd personally strongly advice against that.
Your memory footprint is going to go through the roof, forcing you to implement your own cache expiration mechanism, while Apple has a very good image cache through -imageNamed.
I'd be surprised if you can do a better job than apple on all 3 family of devices :)
Then, you're breaking the flyweight pattern of the UITableView here:
dispatch_sync(dispatch_get_main_queue(), ^{
cell.beerImage.image = image;
beer.image = image;
[cell setNeedsLayout];
});
Ask the table view to give your the cell at a given index rather than capture the cell in the block: by the time the image is loaded, that cell instance might actually have been reused for another index path, and you'll be displaying the image in the wrong cell.
And no need for -setNeedsLayout here, just changing the image is enough.
Edit: whoops! I missed the obvious thing with images in table view. What size are your images, what size is the image view, and what is the content mode on the image?
If your images are of a very different size than the image view and you're asking the imageview to resize, this will happen on the main thread and you'll take a massive performance hit there.
Resize the image to the image view off thread, after loading (a quick google search will give you the core graphics code to do that).
The missing step is to update the model with the fetched image. As it is, you're doing a new load for every cell every time. The model is the right place to cache the result of the relatively expensive load. Can you add a Beer.image property?
Then, your config code would look like this:
Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;
// did we already cache a copy of the image?
if (beer.image != nil) {
// good. use it. this will run quick and this will run most of the time
cell.beerImage.image = beer.image;
} else {
// it must be the first time we've scrolled by this beer. do the expensive
// image init off the main thread
cell.beerImage.image = nil; // set a default value here. nil is good enough for now
[self loadImageForBeer:beer atIndexPath:indexPath];
}
Moved the loader logic here for clarity ...
- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
beer.image = image;
dispatch_sync(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.beerImage.image = image;
});
});
}
You can have a look on this question ,previously answered at stack overflow.
UIImage in uitableViewcell slowdowns scrolling table
or else try this code
- (void)configureCell:(BeerCell *)cell
atIndexPath:(NSIndexPath *)indexPath
{
Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;
UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
cell.beerImage.image = image;
[cell setNeedsLayout];
}
Related
I need to resize a large locally stored image (contained in self.optionArray) and then show it in the collectionView. If I just show it, iOS trying to resize the images as I scroll quickly causing memory-related crashes.
In the code below, the collectionView will scroll smoothly, but sometimes if I scroll extremely fast, there will be an incorrect image that shows and then changes to the correct one as the scrolling decelerates. Why isn't setting the cell.cellImage.image to nil fixing this?
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CustomTabBarCell" forIndexPath:indexPath];
cell.cellImage.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
cell.cellImage.image = nil;
UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
dispatch_sync(dispatch_get_main_queue(), ^{
cell.cellImage.image = localImage2
cell.cellTextLabel.text = #"";
[cell setNeedsLayout];
});
});
}
return cell;
}
- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize {
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
EDIT:
I added another async to cache first and nil and initialized the cell.image. I'm having the same issue on the initial fast scroll down. However, on the scroll back up, it's flawless now.
I added this:
-(void)createDictionary
{
for (UIImage *test in self.optionArray) {
UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
[localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:[self.optionArray indexOfObject:test]]];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
if (!localImageDict) {
localImageDict = [[NSMutableDictionary alloc]initWithCapacity:self.optionArray.count];
}
else {
[localImageDict removeAllObjects];
}
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
[self createDictionary];
});
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CustomTabBarCell" forIndexPath:indexPath];
cell.cellImage.image = nil;
cell.cellImage.image = [[UIImage alloc]init];
if ([localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]]) {
cell.cellImage.image = [localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]];
cell.cellTextLabel.text = #"";
}
else {
cell.cellImage.image = nil;
cell.cellImage.image = [[UIImage alloc]init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
[localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:indexPath.row]];
dispatch_sync(dispatch_get_main_queue(), ^{
cell.cellImage.image = shownImage;
cell.cellTextLabel.text = #"";
[cell setNeedsLayout];
});
});
}
}
return cell;
Taking a closer look at your code sample, I can see the source of your memory problem. The most significant issue that jumps out is that you appear to be holding all of your images in an array. That takes an extraordinary amount of memory (and I infer from your need to resize the images that they must be large).
To reduce your app's footprint, you should not maintain an array of UIImage objects. Instead, just maintain an array of URLs or paths to your images and then only create the UIImage objects on the fly as they're needed by the UI (a process that is called lazy-loading). And once the image leaves the screen, you can release it (the UICollectionView, like the UITableView does a lot of this cleanup work for you as long as you don't maintain strong references to the images).
An app should generally only be maintaining UIImage objects for the images currently visible. You might cache these resized images (using NSCache, for example) for performance reasons, but caches will then be purged automatically when you run low in memory.
The good thing is that you're obviously already well versed in asynchronous processing. Anyway, the implementation might look like so:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CustomTabBarCell" forIndexPath:indexPath];
NSString *filename = [self.filenameArray objectAtIndex:indexPath.row]; // I always use indexPath.item, but if row works, that's great
UIImage *image = [self.thumbnailCache objectForKey:filename]; // you can key this on whatever you want, but the filename works
cell.cellImage.image = image; // this will load cached image if found, or `nil` it if not found
if (image == nil) // we only need to retrieve image if not found in our cache
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *test = [UIImage imageWithContentsOfFile:filename]; // load the image here, now that we know we need it
if (!test)
{
NSLog(#"%s: unable to load image", __FUNCTION__);
return;
}
UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
if (!localImage2)
{
NSLog(#"%s: unable to convert image", __FUNCTION__);
return;
}
[self.thumbnailCache setObject:localImage2 forKey:filename]; // save the image to the cache
dispatch_async(dispatch_get_main_queue(), ^{ // async is fine; no need to keep this background operation alive, waiting for the main queue to respond
// see if the cell for this indexPath is still onscreen; probably is, but just in case
CustomTabBarCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell)
{
updateCell.cellImage.image = localImage2
updateCell.cellTextLabel.text = #"";
[updateCell setNeedsLayout];
}
});
});
}
return cell;
}
This assumes that you define a class property of thumbnailCache which is a strong reference to a NSCache that you'll initialize in viewDidLoad, or wherever. Caching is a way to get the best of both worlds, load images in memory for optimal performance, but it will be released when you experience memory pressure.
Clearly, I'm blithely assuming "oh, just replace your array of images with an array of image filenames", and I know you'll probably have to go into a bunch of different portions of your code to make that work, but this is undoubtedly the source of your memory consumption. Clearly, you always could have other memory issues (retain cycles and the like), but there's nothing like that here in the snippet you posted.
I had a similar problem but went about it a different way.
I also had the issue of "pop-in" as images that were loaded async were flashed as they come in until finally the correct one was shown.
One reason this is happening is that the current indexpath for the cell that was initially dequeued did't match the index of the image you are putting into it.
Basically, if you scroll quickly from 0-19 and the cell you want to update is #20 and you want it to show image #20, but it's still loading images 3, 7, 14 asynchronously.
To prevent this, what I did was track two indices; #1) the most recent indexpath that reflects the actual position of the cell and #2) the index corresponding to the image that is actually being loaded async (in this case should actually be the indexpath you are passing into cellforitematindexpath, it gets retained as the async process works through the queue so will actually be "old" data for some of the image loading) .
One way to get the most recent indexpath may be to create a simple method that just returns an NSInteger for the current location of the cell. Store this as currentIndex.
I then put a couple if statements that checked that the two were equal before actually filling in the image.
so if (currentIndex == imageIndex) then load image.
if you put an NSLog(#"CURRENT...%d...IMAGE...%d", currentIndex, imageIndex) before those if statements you can see pretty clearly when the two do not match up and the async calls should exit.
Hope this helps.
I found the wording of what chuey101 said, confusing. I figured out a way and then realized that chuey101 meant the same.
If it is going to help anyone, images are flashed and changed because of the different threads that are running. So, when you spawn the thread for image operations, its going to get spawned for a specific cell no, say c1. But, at last when you actually load your image into the cell, its going to be the current cell that you are looking at, the one that you scrolled to - say c2. So, when you scrolled to c2, there were c2 threads that were spawned, one fore each cell, as you scrolled. From what I understand, all these threads are going to try loading their images into the current cell, c2. So, you have flashes of images.
To avoid this, you need to actually check that you are loading the image that you want into the cell that you mean to load to. So, get the collectionviewcell indexpath.row before loading image into it (loading_image_into_cell). Also, get the cell for which you spawned off your thread to before you spawn off the thread i.e. in the main thread (image_num_to_load). Now, before loading, check that these two numbers are equal.
Problem solved :)
I have to 20-25 download images of 50 Kb- 2 Mb each and show them in a tableview.
I used ASIHTTPRequest asyn request to this. I observed that after some time the app gets stuck. This should not happen because I am using a async call. I thought something is wrong with ASIHTTPRequest and I observed that The didFinished selector gets called in the main thread. The only thing which I do is
-(void)didFinishedDownloadingImage:(ASIHTTPRequest*)request
{
NSData *responseData = [request responseData];
UIImage *image = [UIImage imageWithData:responseData];
[[data objectAtIndex:request.tag] setImage:image];
[self.tableView reloadData];
}
I don't think this should cause any problem. Also in cellforrowatindexpath I just do
- (UItableViewCell *)tableviewView:(UItableView *)tableview
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UserProfile * user = [data objecAtIndex:indexpath.row];
UITableViewCell *cell = [tableView
dequeueReusableCellWithReuseIdentifier:#"ProfileCell"
forIndexPath:indexPath];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewDefaultStyle];
}
NSString *fullname = [NSString stringWithFormat:#"%#\n%#",
user.firstname, user.lastname];
if(user.image != nil)
[cell.imageView setImage:user.image];
else{
[cell.imageView setImage:[UIImage imageNamed:#"placeholder.jpg"]];
}
[cell.label setText:fullname];
return cell;
}
But the app is slow and freezes for 1-2 sec which is a considerable amount of time.
I have seen apps which does this very smoothly. I tried using an image of fixed size 5Kb which has a very significance performance improvement with using the above code. I don't know why should that make a difference for big images in this case because all downloading is happening in other thread via ASIHTTP .
Please, replace your framework with AFNetworking.
You can simple use..
IImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
[imageView setImageWithURL:[NSURL URLWithString:#"http://i.imgur.com/r4uwx.jpg"] placeholderImage:[UIImage imageNamed:#"placeholder-avatar"]];
or... directly in TableViewCell
NSURL *url = [[NSURL alloc] initWithString:[movie objectForKey:#"artworkUrl100"]];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:#"placeholder"]];
"In the second line, we tell the image view where the thumbnail is located by passing an NSURL and we pass in a placeholder image, which is shown as long as our request has not returned a response"
Thats all!
Here you have an tutorial about that http://mobile.tutsplus.com/tutorials/iphone/ios-sdk_afnetworking/
It's easy to make assumptions about the root cause of a laggy/slow application. Instead of guessing, why don't you test your suspicions? Profile your application with the Time Profiler instrument. It'll tell you which methods and functions your application is spending the most time in.
Here are some ideas until you have a chance to profile:
You might consider downloading the full-res images and creating thumbnails in the background and then caching them in an NSCache object. You can also run [UIImage imageWithData:responseData]; in a background thread. It's thread-safe until the point at which it interacts with the view hierarchy.
Selectively reloading a single cell should be faster than reloading the entire tableview, especially one with lots of images. Furthermore if you're doing all of the networking and processing on a background queue, there's no reason scrolling the tableview should be slow. Can you show us your entire implementation of the -cellForRowAtIndexPath: method? You've mentioned that you think setImage: is your slow point because rendering is slow. If you reload a single cell, only one cell needs to be rendered. If you reload the entire tableview, every cell must be re-rendered.
I have a standard UITableView with standard cells (meaning no modification). Each cell needs to have its text pull from a different web URL:
cell.textLabel.text = [self getTitleFromURL:myURL];
Of course, calling a URL on the main thread is not what you want to do. So how do I do this? I've tried something like (which I got from another StackOverflow post) this but it doesn't work:
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *title = [self getTitleFromURL:myURL];
dispatch_async( dispatch_get_main_queue(), ^{
cell.textLabel.text = title;
});
});
Ideas? I am probably missing something really simple here.
One thing you need to take care of with asynchronous data loading into tables - by the time the data arrives, the cell may have scrolled offscreen, and worse, may have been reused for a different entry in the data array.
So on your return to the main thread you need to check whether the information is still relevant, and make no assumptions as to which cell to post it to...
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
UITableViewCell * correctCell = [self.tableView cellForRowAtIndexPath:indexPath];
correctCell.textLabel.text = title;
[correctCell setNeedsLayout];
}
You also may need to tell the cell to update itself with setNeedsLayout.
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... :)
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.
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;
}
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
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.
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.
I do something like this, but iPad-only, and it seemed fast enough. NSOperationQueue (or raw GCD) seems like the simplest approach, in that everything can be self-contained and you don't need to worry about synchronization. Also, you might be able to save the last operation, and use setQueuePriority: to lower it. Then the most recent one will be pulled from the queue first. Or go through all -operations in the queue and lower their priority. (You could probably do this after completing each one, I assume this would still be significantly faster than doing the work itself.)
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.
I'm a big fan of NSOperationQueue's interface and ease-of-use, but I also needed a LIFO version. I ended up implementing a LIFO version of NSOperationQueue here that has held up quite well for me. It mimics NSOperationQueue's interface, but executes things in a (roughly) LIFO order.
I wonder if anyone can speculate or better yet provide a piece of code as for the implementation of the lengthy friends list in the Facebook iPhone app.
when you open the app and go strait to the friends list, you get the list almost in an instant, at least for me with ~500 friends.
when I try it in my own app it takes lots of precious seconds to populate the table view with the same data, so how does Facebook accomplished such a quick response time ?
upon looking at the tableview in the facebook app you notice there is no scroll bar usually found in such tableview, could that be one sign of the neat trick facebook is utilizing to achieve this rapid rows insert ? could it be they implemented some sort of a virtual tableview with only holds a few dozen rows but rotates them ?
any thoughts ?
the UITableView will let you do this. There are a number of examples on the internet with UITableView and Custom Cell's
Essentially, you load your images in the background, and you reuse the Cells that are in the tableview
EDIT Added example code to demonstrate how this is accomplished.
IMPORTANT NOTE
This code was not tested and may or may not actually function as is.
It was pasted with some editing for length. I did a lot more then this in my app, but in the interest of keeping with the example requested I omitted a lot.
On with the example:
Here is where I get the cell, load it with the items that are readily available. And send it to the background thread to load the rest.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"OfferCell";
static NSString *CellNib = #"OfferItem";
OfferCell* cell = (OfferCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CellNib owner:self options:nil];
cell = (OfferCell*)[nib objectAtIndex:0];
}
NSDictionary* couponPackage = [self.jsonOfferData valueForKey:#"result"];
NSArray *couponList = [couponPackage valueForKey:#"offers"];
if ([couponList count] >= indexPath.row )
{
NSDictionary* couponData = [couponList objectAtIndex:indexPath.row];
Coupon *coupon = [[Coupon alloc] initWithDictionary:couponData];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:cell,#"cell",coupon,#"coupon", nil];
//Right here you would try to load any cached imaged from disk.
//Then send a Thread to the background to load the image.
[self performSelectorInBackground:#selector(loadTableViewCellData:) withObject:params];
//Load up the rest of the custom info into the custom cell.
[cell.captionLabel setText:coupon.name];
[cell.subTextLabel setText:coupon.subText];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
[cell setCommand:coupon.command];
[cell setParameter:coupon.commandArgs];
[cell setImageURL:coupon.imageURL];
[cell setImageAltURL:coupon.imageAltURL];
[cell setRegistrationCode:coupon.registrationCode];
[coupon release];
}
return cell;
}
as you can see, i call a background thread before i even load the custom content in the cell.
- (void) loadTableViewCellData:(NSDictionary*) objectData
{
OfferCell *cell = [objectData objectForKey:#"cell"];
Coupon *coupon = [objectData objectForKey:#"coupon"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[coupon iconURL]]]];
[objectData setValue:image forKey:#"image"];
self performSelectorOnMainThread:#selector(setImageOnMainThread:) withObject:objectData
}
after downloading the image, i send a Main thread request to update the Image that is in the cell object.
- (void) setImageOnMainThread:(NSDictionary*) objectData
{
OfferCell *cell = [objectData objectForKey:#"cell"];
Coupon *coupon = [objectData objectForKey:#"coupon"];
UIImage *image = [objectData objectForKey:#"image"];
cell.icon.image = image;
}
##AGAIN This May not Actually Function. ##
I did not copy all of my code for this. this is a hammer out so you can get the idea.
play with the code and test it. but the fundamentals are.
Dequeue the cell that will fit your needs (Reuse Identifier)
Use the cell if it can be dequeue'd or create a new one with a reuse identifier (my example uses a xib file named OfferItem.xib)
Send a thread to the background that will load the image data from disk or url (a combination of both is recommended)
Send a thread back to the UI when you are ready to load the image into the View (Updating the UI must be done on the main thread)
if you can do that, then your friends list (or in this case offers) will be loaded up as fast as possible. and the Images will pop on the screen as soon as they download.
Also if you use a Caching technique it will be faster for subsequent loads because in the the first method {tableView:cellForRowAtIndexPath:} you would load up the cached image immediately.
Aside from that, this should load your cell's pretty fast.
They obviously load the data from a local resource (plist, ManagedObject, ...)
Have a look at some sample code to draw a TableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *kCellIdentifier = #"MyCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:kCellIdentifier] autorelease];
}
return cell;
}
The dequeueReusableCellWithIdentifier: thing is one reason why TableViews in iOS can draw quickly. It works somehow like this:
1)You provide an identifier for a cell you're creating.
2)Cells that are visible at first get alloced (with identifier)
3)When a Cell is moved off the screen it gets put on a pile MyCellIdentifier
4)Whenever the system needs to draw a cell of identifier:MyCellIdentifier it first looks whether there are any cells currently unused on the MyCellIdentifier pile. If that's the case it picks one off the pile and thus doesn't have to alloc a new one. That way expensive allocing can be kept at a minimum.
I hope this answers your question :)