 |
 |
|
 |
06-12-2009, 02:58 PM
|
#1 (permalink)
|
|
Registered Member
Join Date: Mar 2009
Location: Toronto, ON
Posts: 111
|
Asynchronous image download/caching performance issue
I will try to explain what I am trying to do and what my issue is to my best ability.
I have a UITableView, with 5-6 cells on the screen at any given time. In each cell, I have a different image (depending on the content of the cell). The image file name is obtained from an XML feed (like the rest of the cell content).
TableViewController code:
Code:
// get the string and parse it into a url to create the final image URL
NSString *param = anObject.imageFileName;
NSString *fileName = [NSString stringWithFormat:@"%@.jpg", param];
NSString *urlString = [NSString stringWithFormat: @"http://www.[redacted].com/images%@", fileName];
NSURL *imageURL = [NSURL URLWithString:urlString];
// draw a default image until an image is downloaded from URL
UIImage *blankImage = [[UIImage imageNamed:@"blankImage.png"] retain];
cell.cellImage = blankImage;
[blankImage release];
// draw the image from URL connection or from cache (see code below)
cell.cellImageURL = imageURL;
As you can see above, I get/set the filename (so it knows what to download and what to save/retrieve the image as locally). For pretty UX I just draw a default image before the URL connection has been able to retrieve the image (if an image is found locally the change from default to local image will happen before you see it anyway).
Below is my code in my TableViewCell
Code:
- (void)setCellImageURL:(NSURL*)url
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *homeDirectoryPath = [paths objectAtIndex:0];
NSString *unexpandedPath = [homeDirectoryPath stringByAppendingString:@"/Images/"];
// folderPath is setup in the header
folderPath = [[NSString pathWithComponents:[NSArray arrayWithObjects:[NSString stringWithString:[unexpandedPath stringByExpandingTildeInPath]], nil]] retain];
if (![[NSFileManager defaultManager] fileExistsAtPath:folderPath isDirectory:NULL]) {
[[NSFileManager defaultManager] createDirectoryAtPath:folderPath attributes:nil];
}
fileName = [[url path] lastPathComponent];
fileName = [[folderPath stringByAppendingPathComponent:fileName] retain];
// check for the image in the cache.
if ([[NSFileManager defaultManager] fileExistsAtPath:fileName]) {
UIImage *localImage = [[[UIImage alloc] initWithContentsOfFile:fileName] retain];
self.cellImage = localImage;
[localImage release];
return;
}
if (connection != nil) { [connection release]; }
if (data != nil) { [data release]; }
NSURLRequest* request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData {
if (data == nil) { data = [[NSMutableData alloc] initWithCapacity:2048]; }
[data appendData:incrementalData];
}
- (void)connectionDidFinishLoading:(NSURLConnection*)theConnection {
[connection release];
connection = nil;
self.cellImage = [[UIImage imageWithData:data] retain];
[self setNeedsDisplay];
// save image if it's not already cached
if (cellImage != nil) {
CGFloat compressionQuality = 1.0;
NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation(cellImage, compressionQuality)];
[imageData writeToFile:fileName atomically:YES];
} else {
// if the data == nil, basically, there's no image on the server, then "re"display the default image
self.cellImage = [[UIImage imageNamed:@"blankImage.png"] retain];
}
[cellImage release];
[data release];
data = nil;
}
I've used code from this blog: http://www.markj.net/iphone-asynchronous-table-image/ which helped me setup the asynchronous image downloading, and then I found another source for simply setting up the local folder /Images/ in the app's sandbox (which is done every time the image is called in my tableviewcontroller) and then saved in connectionDidFinishLoading:
Everything works great. EXCEPT, it's slow... I am drawing the cell, so scrolling worked flawlessly until I set up the part where I do the folderPath and save/retrieve the image on disk. I believe that's the part that slow down my table view.
The problem is, I have tried to setup the Directory and folderPath in my AppDelegate, so it just happens on app launch, but then it seems my TableViewCell is unable to get the folderPath correctly. It's just nil! And yes, I am importing the AppDelegate class, etc. correctly, because I am importing the AppDelegate class in my TableViewController and there's it's not nil.
I can provide additional code if needed. Hoping someone out there can assist. Thanks!
|
|
|
06-12-2009, 05:03 PM
|
#2 (permalink)
|
|
Senior Member
iPhone Dev SDK Supporter
Join Date: Jul 2008
Location: San Mateo, CA (San Fran)
Posts: 2,575
|
Quote:
Originally Posted by CanadaDev
The problem is, I have tried to setup the Directory and folderPath in my AppDelegate, so it just happens on app launch, but then it seems my TableViewCell is unable to get the folderPath correctly. It's just nil! And yes, I am importing the AppDelegate class, etc. correctly, because I am importing the AppDelegate class in my TableViewController and there's it's not nil.
|
You're saying that you create folderPath every time you need an image because you tried creating it just once, at the start of the app, but when you tried to use it later it was nil?
That's probably becuase [NSString pathWithComponents] returns an autoreleased string - that string will get released unless you retain it in some way, like putting it in a property. If you've created a property, then this should do the trick:
Code:
self.folderPath = BLAH BLAH
otherwise, you just need a [folderPath retain].
PS - the way you're building the folderpath looks strange - you turn a string into an array with one item into a path. I think this would work just as well:
Code:
self.folderPath =[homeDirectoryPath stringByAppendingPathComponent:@"Images"];
PPS - if things are still slow, consider keeping an array of filenames that you've already loaded - then you could check if an image is cached by checking if it's in the array, instead of hitting the filesystem with fileExistsAtPath.
Also run through your code with the debugger - make sure that, for some reason, it isn't recreating the Images directory every time, or failing to use the cache for some reason.
__________________
|
|
|
06-15-2009, 11:51 AM
|
#3 (permalink)
|
|
Registered Member
Join Date: Mar 2009
Location: Toronto, ON
Posts: 111
|
Quote:
Originally Posted by smasher
You're saying that you create folderPath every time you need an image because you tried creating it just once, at the start of the app, but when you tried to use it later it was nil?
|
No it's not nil. I think the problem is that it's doing too much work when it loops through the code for every cell, or every time it loads the cell (on a scroll).
Quote:
That's probably becuase [NSString pathWithComponents] returns an autoreleased string - that string will get released unless you retain it in some way, like putting it in a property. If you've created a property, then this should do the trick:
Code:
self.folderPath = BLAH BLAH
otherwise, you just need a [folderPath retain].
PS - the way you're building the folderpath looks strange - you turn a string into an array with one item into a path. I think this would work just as well:
Code:
self.folderPath =[homeDirectoryPath stringByAppendingPathComponent:@"Images"];
|
Tried that. Removed the last line of code for the folderPath, and took your advice above. Still works as it should, although still quite slow on device.
Quote:
|
PPS - if things are still slow, consider keeping an array of filenames that you've already loaded - then you could check if an image is cached by checking if it's in the array, instead of hitting the filesystem with fileExistsAtPath.
|
I'll try to set that up today then. Will have to look into how.
Quote:
|
Also run through your code with the debugger - make sure that, for some reason, it isn't recreating the Images directory every time, or failing to use the cache for some reason.
|
I've now changed some stuff around so it only sets up the folder in the AppDelegate, but as far as I can see it only runs once.
|
|
|
06-15-2009, 01:20 PM
|
#4 (permalink)
|
|
Registered Member
Join Date: Oct 2008
Location: Bengaluru
Posts: 123
|
Where exactly do you invoke setCelllImageURL:?
If its in the tableview delegate method objectforcellatrow.. then i suppose it is the wrong place..
A simple solution is
Create a own ImageCache class (it will be handy in your future projects, as it has helped me a lot)
provide a request method to other classes
Use an queue internally to maintain and process the request,
the request should contain the url, the sender object(object responsible to received the image after loading) and some context info to return back to the caller...
process each request in a separate thread
once the request is processed(i.e. image is loaded) then inform the sender object along with context info
now store the image in the cache (either a mutable dictionary or array) through which you can fetch the image if requested in future..
you can check for the size of total images and keep a constraint on the total size of the cache
(Sender object in your case would be your UITableViewController or the appropriate class)
Hope that explains well..
|
|
|
06-15-2009, 03:05 PM
|
#5 (permalink)
|
|
Registered Member
Join Date: Mar 2009
Location: Toronto, ON
Posts: 111
|
Quote:
Originally Posted by bharath2020
Where exactly do you invoke setCelllImageURL:?
If its in the tableview delegate method objectforcellatrow.. then i suppose it is the wrong place..
|
It's invoked in cellForRowAtIndexPath: in my tableviewcontroller class.
Quote:
A simple solution is
Create a own ImageCache class (it will be handy in your future projects, as it has helped me a lot)
provide a request method to other classes
Use an queue internally to maintain and process the request,
the request should contain the url, the sender object(object responsible to received the image after loading) and some context info to return back to the caller...
process each request in a separate thread
once the request is processed(i.e. image is loaded) then inform the sender object along with context info
now store the image in the cache (either a mutable dictionary or array) through which you can fetch the image if requested in future..
you can check for the size of total images and keep a constraint on the total size of the cache
(Sender object in your case would be your UITableViewController or the appropriate class)
Hope that explains well..
|
Okay, I'll look into this. Find it odd there's no code available that already has all this stuff setup. I mean it's a pretty common thing, and a lot of apps use it. Haven't found a complete code only small pieces here and there.
|
|
|
06-29-2009, 10:18 AM
|
#6 (permalink)
|
|
Registered Member
Join Date: May 2009
Posts: 9
|
Hi !
I'm also trying to do somehing like that (like the rss readers do) and it's hard to have a fast scroll, with caching images etc. I also found a tip : have a custom cell view et put a view into it instead of managing directly in your custom cell, right here : Fast Scrolling in Tweetie with UITableView
(And Apple update the code for custom cell)
Have you any tips to give for caching, or improving performance ? thanks
|
|
|
06-30-2009, 07:35 AM
|
#7 (permalink)
|
|
Registered Member
Join Date: Mar 2009
Location: Toronto, ON
Posts: 111
|
Quote:
Originally Posted by ipodishima
Hi !
I'm also trying to do somehing like that (like the rss readers do) and it's hard to have a fast scroll, with caching images etc. I also found a tip : have a custom cell view et put a view into it instead of managing directly in your custom cell, right here : Fast Scrolling in Tweetie with UITableView
(And Apple update the code for custom cell)
Have you any tips to give for caching, or improving performance ? thanks
|
Not sure if you read my post, but that's exactly what I am doing, however I still had scrolling issues. Those are now almost fully resolved, and I simple don't think it's possible for me to do more seeing I use images that are a bit larger that the ones in other apps, plus I have to resize the images on the fly. I optimized my scrolling even further, by compressing the images upon saving them, which helped the load time a bit.
Also if you have ANY NSLogs get rid of them. They slow everything down a lot.
|
|
|
07-01-2009, 05:28 AM
|
#8 (permalink)
|
|
Registered Member
Join Date: May 2009
Posts: 9
|
Yes i read your post. ^^
I didn't try to resize images, but i improved the scrolling performance a little bit adding images in a dictionary.
I posted 'cause i thought you had a better solution.
And yes, it is better when i remove all NSLogs.
Thanks by the way !
|
|
|
07-01-2009, 08:02 AM
|
#9 (permalink)
|
|
Registered Member
Join Date: Mar 2009
Location: Toronto, ON
Posts: 111
|
Quote:
Originally Posted by ipodishima
Yes i read your post. ^^
I didn't try to resize images, but i improved the scrolling performance a little bit adding images in a dictionary.
I posted 'cause i thought you had a better solution.
And yes, it is better when i remove all NSLogs.
Thanks by the way !
|
Well the images I download are a bit bigger than the size I need for the tableviewcell, so I have to resize on the fly (happens automatically, but still requires a bit of processing power, unfortunately)
I have no done a dictionary, but have been thinking about using one. I am going through NSFileManager to check for a local file, which does require a bit more CPU than just checking a dictionary on the fly.
How did you set up your NSDictionary?
Thanks
|
|
|
07-01-2009, 12:24 PM
|
#10 (permalink)
|
|
Registered Member
Join Date: May 2009
Posts: 9
|
So, i do like this : in - (UITableViewCell *)tableView  UITableView *)tableView cellForRowAtIndexPath  NSIndexPath *)indexPath .
Code:
// Asynchrone View yipi
AsyncUIImageView* asyncImage = [[[AsyncUIImageView alloc] initWithFrame:CGRectMake(0, 0, 60, 60)] autorelease];
asyncImage.tag = 999;
if([imageDict objectForKey:[[xmlParser.stories objectAtIndex:storyIndex] objectForKey:@"title"]])
{
// load from dict, ie cache
asyncImage = [imageDict objectForKey:[[xmlParser.stories objectAtIndex:storyIndex] objectForKey:@"title"]];
}
else {
// load photo from web
[asyncImage loadImageFromRawURLMadeOfString:[[xmlParser.stories objectAtIndex:storyIndex] objectForKey:@"article-body"]];
// add into the dict
[imageDict setObject:asyncImage forKey:[[xmlParser.stories objectAtIndex:storyIndex] objectForKey:@"title"]];
}
with a NSMutableDictionary, of course.
I have to improve how i put the photo into the dictionary, because if i save into a .plist, i don't want to save all the photos ^^
++
|
|
|
 |
| Thread Tools |
|
|
| Display Modes |
Linear Mode
|
Posting Rules
|
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts
HTML code is Off
|
|
|
|
» Advertisements |
» Online Users: 281 |
| 19 members and 262 guests |
| cakesy, camtadao, crooksy88, Erle, Eskema, Falcon80, ghanalupo, jojo453, josh, kiancheong, mixer555, odysseus31173, SalvoMaltese, shay_somech, SkodaBuddy, StefanL, Stitch, Susan03, thadre |
| Most users ever online was 779, 05-11-2009 at 09:55 AM. |
» Stats |
Members: 24,280
Threads: 39,072
Posts: 171,337
Top Poster: smasher (2,575)
|
| Welcome to our newest member, Susan03 |
|