views:

125

answers:

4

I'm building a UITableView similar to iPod.app's album browsing view: http://cl.ly/2mWo

I'm importing all the artists and album artworks from the iPod library on first launch. Saving everything to CoreData and getting it back into an NSFetchedResultsController. I'm reusing cell identifiers and in my cellForRowAtIndexPath: method I have this code:

Artist *artist = [fetchedResultsController objectAtIndexPath:indexPath];
NSString *identifier = @"bigCell";

SWArtistViewCell *cell = (SWArtistViewCell*)[tableView dequeueReusableCellWithIdentifier:identifier];

if (cell == nil)
    cell = [[[SWArtistViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];

cell.artistName = artist.artist_name;
cell.artworkImage = [UIImage imageWithData:artist.image];

[cell setNeedsDisplay];

return cell;

My SWArtistViewCell cell implements the drawRect: method to draw both the string and image:

[artworkImage drawInRect:CGRectMake(0,1,44,44)]
[artistName drawAtPoint:CGPointMake(54, 13) forWidth:200 withFont:[UIFont boldSystemFontOfSize:20] lineBreakMode:UILineBreakModeClip];

Scrolling is still choppy and I just can't figure out why. Apps like iPod and Twitter have butter smooth scrolling and yet they both draw some small image in the cell as I do.

All my views are opaque. What am I missing?

EDIT: here's what Shark says:

alt text

I'm not familiar with Shark. Any pointer as of what are these symbols related to? When I look at the trace of these, they all point to my drawRect: method, specifically the UIImage drawing.

alt text

Would it point to something else if the chokehold was the file reading? Is it definitely the drawing?

EDIT: retaining the image

I've done as pothibo suggested and added an artworkImage method to my Artist class that retains the image created with imageWithData:

- (UIImage*)artworkImage {
    if(artworkImage == nil)
        artworkImage = [[UIImage imageWithData:self.image] retain];

    return artworkImage;
}

So now I can directly set the retained image to my TableViewCell as follow:

cell.artworkImage = artist.artworkImage;

I also set my setNeedsDisplay inside the setArtworkImage: method of my tableViewCell class. Scrolling is still laggy and Shark shows exactly the same results.

+1  A: 

At this point your best bet is to use Shark to try and find bottlenecks in your code. Apple has a "Performance Tuning Your Application with Shark" (iTunes link) video that walks you through Shark if you've never used it before. I think you have to be a member of the $99 iPhone developer program to view it though.

Robot K
I finally figured Shark is now part of Instruments. That's why I couldn't use it before. Thanks for that video. Update my question with the results I got. What do you think?
Sam V
What are the dimensions of the image compared to the size of the rect you're drawing it into? Maybe you should shrink the image to 44x44 when you import it into your app.
Robot K
Original image is 88x88. I draw it into a 44x44 for Retina Display, 44x44 on non-retina. Is there a better way to draw an image on Retina?
Sam V
Not that I know of. So much for that idea.
Robot K
Arg, thanks anyway :/
Sam V
Forgot CG was automatically adapting to the device screen scale. So I was saving my images with 88x88 dimensions, which was translating to 176x176 on Retina and 88x88 on non-Retina. Drawing 176x176 images into 44x44 rects is what was giving me the lag. So you were right! Should have checked the imported images dimensions earlier :/
Sam V
A: 

My guess is that the delay is from storing images in Core Data. Core Data is usually not a good way to store large blobs of data.

A better solution would be to store the images as individual files on disk, using an album id to identify each image. Then you would setup an in memory cache to store the images in RAM for fast loading into your UIImageViews. The loading of the images from disk to RAM would ideally need to be done on a background thread (e.g. try performSelectorOnBackgroundThread) so that the I/O doesn't bog down the main thread (which will impact on your scrolling performance).

Addendum 1

If you're only calling -drawRect: once per cell load (as you should be), then the problem might be the scaling of the images. Scaling an image by drawing it in code using drawInRect will use CPU time, so an alternative approach is to scale the images as you receive them from the iPod library (if you need them in multiple sizes, save a version in each size you require). You may need to do this on a background thread when you import the data to avoid blocking the main (UI) thread.

One alternative thing to consider is that UIImageView may do it's scaling using Core Animation which would mean it is hardware accelerated (I'm not sure if this is actually the case, I'm just guessing). Switching to a UIImageView for the image would therefore get rid of the CPU burden of image scaling. You would have a slight increase in compositing overhead, but it might be the easiest way to get closer to "optimum" scrolling performance.

Nick Forge
Are you familiar with Shark? Does my edited answer point out that the chokehold might indeed be from the storing of images in CoreData?
Sam V
No, it looks more like your problem is that you're drawing too often. Is it possible that you are calling `-drawRect:` too often (by setting `setNeedsDisplay`)? Try putting an `NSLog()` message in your `-drawRect:`, it should only get called when a new cell is loaded. If it's called at any other time, that's your problem (or at least one of your problems).
Nick Forge
Yea that was one of my guess. But no, drawRect is only getting called once. I do find it odd having to call setNeedsDisplay, usually I dont need to do that.
Sam V
It could be the scaling of the images when you draw them - see Addendum 1 above.
Nick Forge
Good point. The only thing is that my 88x88 image isn't scaled when I draw it into the 44x44 rect. That's how you draw images on the Retina display anyway.
Sam V
A: 

If the delay's happening in your -drawRect, then you might want to take a look at this article: Tweetie's developer explains pretty thoroughly the method he used to get that smooth scrolling you're after. This has become a bit easier since then, though: CALayer has a shouldRasterize property that basically flattens its sublayers into a bitmap, which can then—as long as nothing changes inside the layer—give you much better performance when animating the layer around, as UITableView does when you scroll it. In this case, you'd probably apply that property to your individual UITableViewCells' layers.

Noah Witherspoon
That shouldRasterize property is nowhere to be found in the FastScrolling project. What ABTableView introduces is indeed drawing content directly, instead of allocating UILabels and UIImageView. This is exactly what I'm doing.
Sam V
+2  A: 

[UIImage imageWithData:] doesn't cache.

This means that CoreGraphic uncompress and process your image every single time you pass in that dataSource method.

I would change your artist's object to hold on a UIImage instead of NSData. You can always flush the image on memoryWarning if you get them a lot.

Also, I would not recommend using setNeedsDisplay inside the dataSource call, I would use that from within your cell.

SetNeedsDisplay is not a direct call to drawRect:

It only tells the os to draw the UIVIew again at the end of the runloop. You can call setNeedsDisplay 100 times in the same runloop and the OS will only call your drawRect method once.

Pier-Olivier Thibault
Problem is you can't store an UIImage in CoreData. Do you suggest I should save the image to disk instead? And I should call [self setNeedsDisplay] inside the setArtworkImage: method for example?
Sam V
Oh or should I do imageWithData inside my Artist object and retain it for future uses?
Sam V
It's not recommended to store image in CoreData at all (via NSData or not) Of course, if the size of images are small it's okay, but otherwise, it's similar to using BLOBs in mySQL. You might want to use NSKeyArchiver to store the image and only store the filename in CoreData.
Pier-Olivier Thibault
imageWithData inside your Artist Objects would be my way to go. Remember, CoreData is only a backstore. you can feel free to subclass your method - (NSData *)artworkImage; to a - (UIImage *)artworkImage where you fetch the image in the backstore and store it in a private variable if it's not already. I don't know if you follow.
Pier-Olivier Thibault
Yep yep, I follow. So storing/fetching 88x88 images from disk is definitely faster than fetching from CoreData?
Sam V
Will avoid future problems with CoreData (queries can become slower if you have loads of BLOB) I would definetly store the image on disk. if images become a pain, you'll be able to fetch the image from a different thread eventually. If you stick everything in coreData, you won't be able to do much to optimize.
Pier-Olivier Thibault
You're my hero. Need to implement all of these changes, but this all makes sense. Thanks a lot!
Sam V
Is it same lag or faster? Also, try to check if you create a new object everytime you get an artist via Artist *artist = [fetchedResultsController objectAtIndexPath:indexPath];I'm not sure how CoreData works as I have used it only once. If you instantiate a new Artist everytime, the problems remain the image creation.Also, Sharks' analytics data points to an image decompression operation so I have to think it's a UIImage being decompress everytime.
Pier-Olivier Thibault
That imageWithData code only gets executed once per cell (I put a log inside the if). Same lag. Yea Sharks points to image decompression but since I retain the image, shouldnt that avoid it to get decompressed everytime it gets drawn?
Sam V
Well if you go through imageWithData EVERY time you load a cell, you have something not working there, you don't want to load images while you scroll. Scrollviews are already using a lot of processing, it's normal you will get lag. Have you tried loading the image in your init instead of your getter?
Pier-Olivier Thibault