UIImageView category to download images with GCD fails with a large number simultaneous downloads - objective-c

I've used AFNetworking to download and cache images in my project, but I wanted to replace the framework with a lightest in-house category, obviously I'm facing a lot of issues with dispatch queues, and I'm having a hard time debugging them.
This is the category I'm using right now, It works with no problem with a small number of simultaneous downloads but, if I start a lot of downloads it looks like the application hangs an an endless load.
I need some help debugging and I would like to understand what I'm missing here.
Here are the two main functions:
- (void) imageWithUrl: (NSURL*) imageUrl placeHolderImage: (UIImage *) placeHolderImage shouldAlwaysRefresh: (BOOL) shouldRefresh {
self.image= placeHolderImage;
TMCache *sharedCache = [TMCache sharedCache];
[sharedCache trimToDate: [NSDate dateWithTimeIntervalSinceNow: -(60.0*24.0*7.0)]];
UIImage *cachedImage = [sharedCache objectForKey: [imageUrl absoluteString]];
if(cachedImage){
self.image = cachedImage;
if(shouldRefresh)
[self fetchImageFromUrl: imageUrl];
}
else{
[self fetchImageFromUrl: imageUrl];
}
}
- (void) fetchImageFromUrl: (NSURL *) imageUrl{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
UIImage *image = [UIImage imageWithData:imageData];
if(image){
[[TMCache sharedCache] setObject:image forKey: [imageUrl absoluteString]];
dispatch_sync(dispatch_get_main_queue(), ^{
self.image = image;
});
}
});
}

Your problem is most likely the dispatch_sync back to the main queue creating a deadlock when the image has finished loading.
The easiest way to solve that is to simply replace your dispatch_sync with dispatch_async.
if(image) {
[[TMCache sharedCache] setObject:image forKey: [imageUrl absoluteString]];
dispatch_async(dispatch_get_main_queue(), ^{
self.image = image;
});
}
I don't see any reason to use a dispatch_sync there since you're already in a dispatch_async block; this shouldn't have any negative effects on your code. If fact, you should usually try to avoid using dispatch_sync if at all possible, precisely for the issue you're running in to here.
Sidenote: AFNetworking actually gives you quite a bit for not all that much code. I'd seriously think twice before using your own solution over AFNetworking.

Related

How to implement lazy loading of image without using SDImageCache?

How to implement lazy loading of image without using below link:
https://github.com/rs/SDWebImage
Note: I don't want to use any third party tool. I want to do from my side.
If you don't want to use any library (some are MIT or public domain licensed, so I think the only reason not to use them is that you want to learn how to build it yourself).
Here is how to do it with a simple and effective way:
1 : Put a temporary placeholder image in your imageView.
2 : Get your image in background thread.
2-a : If you want the cache feature, search for a cached image.
2-b : If no cache feature or no cached image, get your image from its source.
2-c : If cache feature, save the image to cache.
3 : In main thread show your image in the imageView.
Pseudo Code : (I wrote it on the go, it is not meant to run and it may have errors, sorry for that).
-(void) lazilyLoadImageFromURL :(NSURL *)url{
imageView.image = [UIImage imageNamed:#"Placeholder.png];
if([self cachedImageAvailableForURL:url){
imageView.image= [self cachedImageForURL:url];
}
else{
NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse * resp, NSData *data, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),^
{
if ( error == nil && data )
{
UIImage *urlImage = [[UIImage alloc] initWithData:data];
imageView.image = urlImage;
[self saveImageInCache:image forURL:url];
}
});
}];
}
}
-(BOOL) cachedImageAvailableForURL:(NSURL*):url{
// check if there is a saved cached image for this url
}
-(UIImage *) cachedImageForURL:(NSURL*):url{
// returns the cached image for that url
}
-(void) saveImageInCache:(UIImage*) image forURL:(NSURL*)url{
// saves the image in cache for the url
}
Of course, this is only ONE POSSIBLE WAY to do it. I tried to make it simple, but there are plenty better and more complicated ways to do it.
Try it:
NSURL *imageURL = [NSURL URLWithString:#"www...."];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
self.imgVWprofile.image=[UIImage imageWithData:imageData];
});
});

How to retrieve images from server asynchronously

i have one NSMutableArray with some image url's. The images have sizes between 12KB to 6MB. I use AsycImageView class and implement but when large images are downloading application get crashed, I gave 6*1024*1024 (6MB) for maxsize in that class, increase time interval 60.0 sec to 180.o sec, but there is no use. I'm getting an error "Received memory warning" and when app crash automatically connection remove from device, but in simulator there is no crash.
Use GCD for lazy loading.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSString *strURL = url here;
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:strURL]];
UIImage *image = nil;
if(data)
image = [UIImage imageWithData:data];
dispatch_sync(dispatch_get_main_queue(), ^{
//now use image in image View or anywhere according to your requirement.
if(image)
yourImgView = image
});
});
you can do this using multiThreading. Here is a code
- (UIImageView *)getImageFromURL:(NSDictionary *)dict
{
#ifdef DEBUG
NSLog(#"dict:%#", dict);
#endif
UIImageView *_cellImage = nil;
_cellImage = ((UIImageView *)[dict objectForKey:#"image"]);
NSString *strURL = [dict objectForKey:#"imageurl"]);
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:strURL]];
#ifdef DEBUG
NSLog(#"%i", data.length);
#endif
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [NSString stringWithFormat:#"%#.png", [documentsDirectory stringByAppendingPathComponent:[dict objectForKey:#"imageid"]]];
if (data) // i.e. file exist on the server
{
[data writeToFile:dataFilePath atomically:YES];
_cellImage.image = [UIImage imageWithContentsOfFile:dataFilePath];
}
else // otherwise show a default image.
{
_cellImage.image = [UIImage imageNamed:#"nouser.jpg"];
}
return _cellImage;
}
And call this method in cellForRowAtIndexPath like this:
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:imageURL, #"imageurl", self.imgPhoto, #"image", imageid, #"imageid", nil];
[NSThread detachNewThreadSelector:#selector(getImageFromURL:) toTarget:self withObject:dict];
The code will start getting images in multiple threads and will save image locally to document folder. Also the image will not download again if already exists with the same name. Hope this helps
You could download image asynchronously using GCD. Use the following code,
__block NSData *imageData;
dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, NULL);
dispatch_async(myQueue, ^{
//load url image into NSData
imageData = [NSData dataWithContentsOfURL: your_image_URL];
if(imageData) {
dispatch_sync(dispatch_get_main_queue(), ^{
//convert data into image after completion
UIImage *img = [UIImage imageWithData:imageData];
//do what you want to do with your image
});
} else {
NSLog(#"image not found at %#", your_image_URL);
}
});
dispatch_release(myQueue);
For further info, see dispatch_queue_t
I would recommend a drop in replacement API SDWebImage it provides a category for UIImageVIew with support for remote images coming from the web. you can also have a placeholder image till the images are downloaded Asynchronously
. Its easy to use and saves a lot of work
You're problem seems to be more of a memory usage issue than it is a performance issue.
If you really want to download image asynchronously I would recommend you use The UIImageView category from AFNetworking which has been fully tested and is very well maintained.
However here you run into memory warnings on your device (which obviously holds much less memory than your simulator which runs on your Mac).
So I would use first the static analyzer:
to see if leaks are present and then run a Leaks Instrument to track it down.
Hope this helps.

Loading an image from a URL but displaying it progressively

I have a screen that will load around 5 images, but they are huge images. Right now I use a
NSURLRequest
and a:
connectionDidFinishLoading
..for callback to tell me when each image is loaded.
The problem is that images would pop up one by one. Is there a way to have it display the image while it loads?
Thanks
The guts of what you need to do this are available as CGImageSource methods.
First, you use an asynchronous NSURLConnection to get the data. You add received data to a NSMutableData object as it arrives, so the data object gets bigger and bigger til finished.
You also create a progressive image source:
CGImageSourceRef imageSourcRef = CGImageSourceCreateIncremental(dict);
You will find lots of examples here and on google how to set the dictionary required.
Then as the data arrives, you pass the TOTAL data object into this method:
CGImageSourceUpdateData(imageSourcRef, (__bridge CFDataRef)data, NO); // No means not finished
You can then ask the image source for an image, which will be partial as the image is downloading. With a CGImage you can create a UIImage.
When you get the final data, you update the image source on last time:
CGImageSourceUpdateData(imageSourcRef, (__bridge CFDataRef)data, YES);
You then use the image source to get a final image and you're done.
Displaying it while loading ,I don't think UIImageView can load UIImageswith incomplete data while loading.I will go for
AsyncImageView ,
It can deal with all the burden of loading image asynchronous.Also UIActivityIndicator is already added to it.So it will be more user friendly
Use blocks and GCD's dispatch_async method.
Look at this example:
//communityDetailViewController.h
#interface communityDetailViewController : UIViewController {
UIImageView *imgDisplay;
UIActivityIndicatorView *activity;
// the dispatch queue to load images
dispatch_queue_t queue;
}
#end
//communityDetailViewController.m
- (void)loadImage
{
[activity startAnimating];
NSString *url = #"URL the image";
if (!queue) {
queue = dispatch_queue_create("image_queue", NULL);
}
dispatch_async(queue, ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
UIImage *anImage = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
[activity stopAnimating];
activity.hidden = YES;
if (anImage != nil) {
[imgDisplay setImage:anImage];
}else{
[imgDisplay setImage:[UIImage imageNamed:#"no_image_available.png"]];
}
});
});
}
You can subclass UIImageView and use this.
-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
imageData = [NSMutableData data];
imageSize = [response expectedContentLength];
imageSource = CGImageSourceCreateIncremental(NULL);
}
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
[imageData appendData:data];
CGImageSourceUpdateData(imageSource, (__bridge CFDataRef)imageData, ([imageData length] == imageSize) ? true : false);
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
if (cgImage){
UIImage* img = [[UIImage alloc] initWithCGImage:cgImage scale:1.0f orientation:UIImageOrientationUp];
dispatch_async( dispatch_get_main_queue(), ^{
self.image = img;
});
CGImageRelease(cgImage);
}
}

Reduce lag with UITableView and GCD

I have a UITableView consisting of roughly 10 subclassed UITableViewCells named TBPostSnapCell. Each cell, when initialised, sets two of its variables with UIImages downloaded via GCD or retrieved from a cache stored in the user's documents directory.
For some reason, this is causing a noticeable lag on the tableView and therefore disrupting the UX of the app & table.
Please can you tell me how I can reduce this lag?
tableView... cellForRowAtIndexPath:
if (post.postType == TBPostTypeSnap || post.snaps != nil) {
TBPostSnapCell *snapCell = (TBPostSnapCell *) [tableView dequeueReusableCellWithIdentifier:snapID];
if (snapCell == nil) {
snapCell = [[[NSBundle mainBundle] loadNibNamed:#"TBPostSnapCell" owner:self options:nil] objectAtIndex:0];
[snapCell setPost:[posts objectAtIndex:indexPath.row]];
[snapCell.bottomImageView setImage:[UIImage imageNamed:[NSString stringWithFormat:#"%d", (indexPath.row % 6) +1]]];
}
[snapCell.commentsButton setTag:indexPath.row];
[snapCell.commentsButton addTarget:self action:#selector(comments:) forControlEvents:UIControlEventTouchDown];
[snapCell setSelectionStyle:UITableViewCellSelectionStyleNone];
return snapCell;
}
TBSnapCell.m
- (void) setPost:(TBPost *) _post {
if (post != _post) {
[post release];
post = [_post retain];
}
...
if (self.snap == nil) {
NSString *str = [[_post snaps] objectForKey:TBImageOriginalURL];
NSURL *url = [NSURL URLWithString:str];
[TBImageDownloader downloadImageAtURL:url completion:^(UIImage *image) {
[self setSnap:image];
}];
}
if (self.authorAvatar == nil) {
...
NSURL *url = [[[_post user] avatars] objectForKey:[[TBForrstr sharedForrstr] stringForPhotoSize:TBPhotoSizeSmall]];
[TBImageDownloader downloadImageAtURL:url completion:^(UIImage *image) {
[self setAuthorAvatar:image];
}];
...
}
}
TBImageDownloader.m
+ (void) downloadImageAtURL:(NSURL *)url completion:(TBImageDownloadCompletion)_block {
if ([self hasWrittenDataToFilePath:filePathForURL(url)]) {
[self imageForURL:filePathForURL(url) callback:^(UIImage * image) {
_block(image); //gets UIImage from NSDocumentsDirectory via GCD
}];
return;
}
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_async(dispatch_get_main_queue(), ^{
[self writeImageData:UIImagePNGRepresentation(image) toFilePath:filePathForURL(url)];
_block(image);
});
});
}
First thing to try is converting DISPATCH_QUEUE_PRIORITY_HIGH (aka ONG MOST IMPORTANT WORK EVER FORGET EVERYTHING ELSE) to something like DISPATCH_QUEUE_PRIORITY_LOW.
If that doesn't fix it you could attempt to do the http traffic via dispatch_sources, but that is a lot of work.
You might also just try to limit the number of in flight http fetches with a semaphore, the real trick will be deciding what the best limit is as the "good" number will depend on the network, your CPUs, and memory pressure. Maybe benchmark 2, 4, and 8 with a few configurations and see if there is enough pattern to generalize.
Ok, lets try just one, replace the queue = ... with:
static dispatch_once_t once;
static dispatch_queue_t queue = NULL;
dispatch_once(&once, ^{
queue = dispatch_queue_create("com.blah.url-fetch", NULL);
});
Leave the rest of the code as is. This is likely to be the least sputtery, but may not load the images very fast.
For the more general case, rip out the change I just gave you, and we will work on this:
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_async(dispatch_get_main_queue(), ^{
[self writeImageData:UIImagePNGRepresentation(image) toFilePath:filePathForURL(url)];
_block(image);
});
});
Replacing it with:
static dispatch_once_t once;
static const int max_in_flight = 2; // Also try 4, 8, and maybe some other numbers
static dispatch_semaphore_t limit = NULL;
dispatch_once(&once, ^{
limit = dispatch_semaphore_create(max_in_flight);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(limit, DISPATCH_TIME_FOREVER);
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
// (or you might want the dispatch_semaphore_signal here, and not below)
dispatch_async(dispatch_get_main_queue(), ^{
[self writeImageData:UIImagePNGRepresentation(image) toFilePath:filePathForURL(url)];
_block(image);
dispatch_semaphore_signal(limit);
});
});
NOTE: I haven't tested any of this code, even to see if it compiles. As written it will only allow 2 threads to be executing the bulk of the code in your two nested blocks. You might want to move the dispatch_semaphore_signal up to the commented line. That will limit you to two fetches/image creates, but they will be allowed to overlap with writing the image data to a file and calling your _block callback.
BTW you do a lot of file I/O which is faster on flash then any disk ever was, but if you are still looking for performance wins that might be another place to attack. For example maybe keeping the UIImage around in memory until you get a low memory warning and only then writing them to disk.

how to make a simple multithreading?

Okay, now I was making a mobile application like restaurant finder, I want to show a photo of the restaurant
the example : restaurant x <image x>
It's a code:
if (ImageToDisplay != nil)
{
NSData * imageData = [[[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: ImageToDisplay.URL]]autorelease];
ImageForRestaurant.image = [UIImage imageWithData: imageData];
}
The problem is this process of downloading pictures may take too long. So I want the process to be run on a different thread.
That way the code after that can run without having to wait this one to finish.
How can I do so?
if (ImageToDisplay != nil) {
[self performSelectorInBackground:#selector(loadImage:) object:ImageToDisplay];
}
- (void)loadImage:(ImageToDisplay *)image { //Background method
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSData * imageData = [[[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: ImageToDisplay.URL]]autorelease];
[self performSelectorOnMainThread:#selector(setImageForRestaurant:) withObject:imageData waitUntilDone:NO];
[pool release];
}
- (void)setImageForRestaurant:(NSData *)imageData { //Change UI in main thread
ImageForRestaurant.image = [UIImage imageWithData: imageData];
}
I've just included basics in multi-threading; I guess it will serve your purpose
Did you try
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
I think it's the easiest way.
Use the ASIHTTPRequest library. See 'Creating an asynchronous request' on this page: http://allseeing-i.com/ASIHTTPRequest/How-to-use
Also the new library AFNetworking looks promising. As they say:
If you're tired of massive libraries that try to do too much, if you've taken it upon yourself to roll your own hacky solution, if you want a library that actually makes iOS networking code kinda fun, try out AFNetworking.