views:

115

answers:

3

I've been using @atebits's ABTableViewCell for a while. For those who are unfamiliar with it, basically it makes tableview scrolling much faster by drawing all the labels and images, instead of allocating UIKit objects (such as UILabels, UIImageView, etc).

Now, I have pretty big UITableViews and in each cell, there's a small (32x32) user image and a medium (75x75) photo thumbnail. The drawing really helps on making this tableview scroll fast, but I'm still trying to figure what's the best way to fetch these images that are to be drawn inside the cells.

I've looked at Apple's LazyTableView example, but I'm still not sure what's the best way to do this. All I know is the less code/allocation you have in cellForRowAtIndexPath:, the better it is.

So my images either get downloaded from the server or get retrieved from my CoreDataHelper. My question is: which of the following methods is better to retrieve the cached images?

  1. Use NSFetchRequest in cellForRowAtIndexPath: to fetch the cached image (therefore allocating NSFetchRequest instances as the tableview is scrolling).

  2. Retain all the cached images from the start within the tableView's datasource, that way I can just fetch them off the (already allocated array in viewDidLoad:) datasource in my UITableViewController. This method clearly takes up more memory since it allocates a bunch of small images, but I'm guessing the scrolling would end up being much smoother.

Can anyone enlight me on this?

(By the way, I want the images to appear as the tableview is scrolling. Waiting for it to stop scrolling to load the images is not an option).


EDIT: loading images from disk

Following the provided answers, I'm now trying to load the images directly from disk (after they get saved, obviously) using the image local path, which is part of the tableview datasource. Here's the code inside the cellForRowAtIndexPath: method:

NSData *imageData = [NSData dataWithContentsOfFile:imageLocalPath];     
UIImage *userImage = [UIImage imageWithData:imageData];
cell.avatarImage = userImage;

It's still not butter smooth (there's a small lag every time a new cell appears (everytime cellForRowAtIndexPath: is called) Should I be making this call asynchronously? Or is disk reading quick enough and is definitely not the issue here?


EDIT: More details

1. Where are you getting the imageLocalPath?

The image url is part of my dataSource, which is an NSArray allocated in viewDidLoad (cached into CoreData but that shouldn't affect scrolling). The app documents directory path is also retained in viewDidLoad, so the whole filepath is computed in cellForRowAtIndexPath using

NSArray *urlArray = [picURLString componentsSeparatedByString: @"/"];
NSString *filePath = [NSString stringWithFormat:@"%@/%@", documentsPath, [urlArray lastObject];

2. What happens when you run this through shark?

I never used Shark before, tried to use it and apparently iOS 4 no longer supports it. Can you confirm this? When I hit the "Start" button, nothing happens.

3. What FPS are you getting in Instruments on the device?

I'm getting between 25 and 30fps, but it really doesn't look this smooth on the actual screen. Not sure if this means something. I feel like I'm getting 10-15fps. Again, scrolling is smooth between the cells, it's just when the tableview hits a new cell that there's a slight lag (so fast scrolling = many of those small lags every second).

A: 

I'm not familiar with using CoreData that much but I do think CoreData's as some sort of caching so wether you cache it yourself or you use NSFetchRequest, it will end up being in memory for fast access. Also, I don't really like CoreData since it is a db in itself that has to be kept up to date against the server which ends up being inefficient (well this is my own experience)

Personnally I'd go with something along the line of #2. Since you can use something like ASIHTTPRequest framework to do all your transfer operations which supports caching and multiple other options (like queue).

On a side note, the issue with images appearing after the scrolling stop is simply due to the UIScrollView scrolling operations happening outside of the Main run loop, you can easily fix this by calling your drawing operations on the current run loop and they will appear as it scrolls (Which will obviously affect performances if you do some heavy stuff on every cells)

Pier-Olivier Thibault
Scrolling happens on the main thread. The animation does not, but you get scrolling callbacks.
tc.
Yes, it happens on the main thread, but not on the main _run loop_
Pier-Olivier Thibault
+1  A: 

Retaining all of your images for reference is not going to be scalable. It won't take much to kill an older iPod that way.

CoreData is just teh slowzor in my experience. It wouldn't be a good idea to pipe CoreData objects into a tableview on scroll.

It's my interpretation that you are storing binary images in CoreData? That could be your problem. You might need to just trust disk I/O here. I am the lead on an application that has a tableview with thumbnails twice as big as yours, I do all my caching manually by writing the data to a file on disk and retrieving it... and if it's not in cache I use ASIHTTPRequest to pull.

It's butter smooth on the iPhone, even when having to go to the web for some of the images. As long as you do the cache read asynchronously, you'll be alright.

Jasconius
Yes, right now binary images are stored in CoreData. Writing them to disk sounds like the real deal. Any chance you can share a snippet of how you're storing and fetching the images?
Sam V
Core Data is not slow unless you are doing it wrong. Performing a `NSFetchRequest` in the `-cellForRowAtIndexPath` for example is doing it wrong.
Marcus S. Zarra
@Jasconius I'm really curious of how you're doing it so that it's butter smooth. Can you look at the code in my edited question and let me know if it resembles what you do?
Sam V
@Marcus yeah that's kinda what I meant.. fetching in a cellForRow sounds like what he is doing as his images are core data. I mean... how else would you do it if you images were in CoreData?@Sam what you put in your question is pretty close. What I have is a method called "check cache" that returns a UIImage, and if the UIImage is nil (meaning it wasn't in the cache), I hit the web for it.I actually happen to do the cache check on the main thread, it's that fast.
Jasconius
As an addendum, ASIHTTPRequest recently released a caching module and I think it would be worth checking it out. Basically manages your cache for you and lightens up your own code a little.
Jasconius
He already has the Core Data object in memory so there is no fetching required, just access it. Let Core Data handle the data, that is what it is designed to do. As an aside, ASIHTTPRequest is far heavier than spending 10 minutes and implementing your own delegate on `NSURLConnection` -- I do not recommend it.
Marcus S. Zarra
+1  A: 

Are you certain that the images are your issue? Have you done an alpha check on your table? In my experience, loading images is almost never the performance it for table scrolling, it is almost always alpha channel issues.

I would turn on the alpha test in Instruments first before going down this road.

Update

I wonder if your loading into data first is a hinderance:

UIImage *userImage = [UIImage imageWithContentsOfFile:imageLocalPath];
[cell setAvatarImage:userImage];

That should let UIImage do some caching work for you.

  • Where are you getting the imageLocalPath?
  • Is that stored in a Core Data entity or is there a calculation going on?
  • What happens when you run this through shark?
  • What FPS are you getting in Instruments on the device?

Update 2

Why are you taking Core Data objects and storing them into an NSArray? You should be using a NSFetchedResultsController in most situations.

You should not be constructing the filePath, that takes time and -componentsSeparatedByString: is doing a string search; very slow. Store the file path when you save the image. Much faster. If you don't want to change your model, make it a transient value that is resolved on -awakeFromFetch:.

The functions of Shark are integrated with Instruments as of iOS4.

25-30 FPS is not ideal but is pretty good. That means you are getting good frame rate and minor tweaks (as to major fundamental issues) will probably get it higher.

Marcus S. Zarra
All my tableviews are green. That's a nice test though. Thanks anyway.
Sam V
I am willing to bet that loading the images is still not the cause of the performance issue. Since your tableview is green then the next step would be to pre-load the images and see if that makes a performance difference, if it doesn't (as I suspect) then you can cross it off the list.
Marcus S. Zarra
I just tested the scrolling with preloaded images (for the 2nd time) and scrolling dramatically improves. My question was specific to the image caching/fetching because I already had conducted both the alpha/opaque and the preloaded image tests (although I might have missed one).
Sam V
Interesting, would help to post the code. But in the meantime: I would store the images on disk and put a file reference to them in your object that the cell is representing. Instead of using a `NSFetchRequest` (which is going to slow things WAY down), just grab the path from the object and load it from disk. `UIImage` will cache the image internally (somewhat).
Marcus S. Zarra
Hey thanks for the followup. Posted some code above. Let me know what you think can be wrong.
Sam V
Answer updated.
Marcus S. Zarra
Question updated with answers to your questions :P
Sam V
Updated again, really wish SO would notify the OP when an answer has been updated.
Marcus S. Zarra