How to asynchronously download files in a specific order in iOS? - ios

My iPhone app makes a asynchronous request with NSURLRequest to my site.
The response is JSON with 10 URLs to images. When I receive the asynchronous response I want to download the 10 files using a loop, creating an NSMutableArray of UIImage objects.
I tried to do this with the method NSData dataWithContentsOfURL, and it works but isn't asynchronous, so the user interface is blocked.
If I try to use the NSURL asynchronous method inside the response of this asynchronous method, when I receive the 10 responses with the images, I can't know if the images have been downloaded in order, and in my application the order is important.
What is a solution for downloading files in order, without blocking the UI?
My code:
// Create the request.
NSString *advertURL = [NSString stringWithFormat: #"http://www.myURL"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:advertURL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create url connection and fire request
imagesConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
When I receive response:
//decode json with the urls
NSArray* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
//star the loop to downloading the ten images
for (int i=0; i<10; i++) {
//find image[i] url from json
NSString *fullImage_URL = json[i][#"url"];
//download image synchronously (this blocks the UI!!)
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullImage_URL]];
//insert image in the array
arrayImages[i] = [UIImage imageWithData:data];
}
Then, when I'm sure the array has 10 images and they're in order, I can start to show images on the screen.

Instead of processing the JSON array and then looping and fetching all those images on the main thread, why not do it on a background thread. In the background, run the loop, and at the end, call another method on the main to signal the completed task.
//star the loop to downloading the ten images ... in the background
dispatch_queue_t currentQueue = dispatch_get_current_queue();
dispatch_retain(currentQueue);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0, ^(void) {
for (int i=0; i<10; i++) {
//find image[i] url from json
NSString *fullImage_URL = json[i][#"url"];
//download image synchronously (this blocks the UI!!)
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullImage_URL]];
//insert image in the array
arrayImages[i] = [UIImage imageWithData:data];
}
// Return to main queue
dispatch_async(currentQueue, ^{
// process arrayImages now
});
dispatch_release(currentQueue);
});
To read more about the dispatch queues, check this out:
https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW1
There are many other ways to do this. Personally, I'm a fan of using NSNotificationCenter myself, and the OBSERVER pattern.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/Reference/Reference.html

Related

Loading multiple images in background thread in iOS

I am creating an iphone app, where I am trying to fetch multiple (5-7) hi-res images from backend server using URL. All fetched images I need to set in a slideshow, now the problem is that very first time if I am fetching images from server then my images are not displaying. Instead of images, am getting white background. Again if I go back and fetching the same images then it's displaying correctly.
Here is my code :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
for (int i=0; i<[end.slideShowArray count]; i++) {
SlideShow *pro = [[SlideShow alloc] init];
pro = [end.slideShowArray objectAtIndex:i];
NSURL *url = [NSURL URLWithString:pro.imageUrl];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *img = [UIImage imageWithData:imageData];
dispatch_async (dispatch_get_main_queue (),
^{
[slideshow addImage:img];
});
}
});
You're creating a new SlideShow every time around the loop. I think you intended to create a single slide show then add all the images to it.

Memory pressure downloading many images

I am trying to download thousands of pictures (Maximum 350 Kb each) from the server, but I go by over a thousand images I receive the alert "Memory Presure".
Basically I have an array with all the names of the images and do a loop to bring one to one like this:
for (int x=0; x<unique.count; x++) {
NSURL *ImageLink = [NSURL URLWithString:[NSString stringWithFormat:#"http://urltoimagesfolder.com/", [unique objectAtIndex:x]]];
NSData *data = [NSData dataWithContentsOfURL:ImageLink];
UIImage *img = [[UIImage alloc] initWithData:data];
if (data.length !=0) {
NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:[unique objectAtIndex:x]]; //add our image to the path
[UIImageJPEGRepresentation(img, 1.0) writeToFile:fullPath atomically:YES];
//[self saveImage:img :NombreFoto];
//[self Miniatura:img :[NSString stringWithFormat:#"mini-%#", [unique objectAtIndex:x]]];
}
data = nil;
img = nil;
}
Question: How I can download all the images without the app crash with memory pressure?
UIImageJPEGRepresentation() may cause the memory overflow.
But you don't need to use that function, you can check if the received data is image and write its bytes directly to disk via sending message writeToFile: to data object.
You can fix you code like this:
for (int x=0; x<unique.count; x++) {
NSURL *ImageLink = [NSURL URLWithString:[NSString stringWithFormat:#"http://urltoimagesfolder.com/", [unique objectAtIndex:x]]];
NSData *data = [NSData dataWithContentsOfURL:ImageLink];
if (data.length !=0) {
UIImage *img = [[UIImage alloc] initWithData:data];
if (img) {
NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:[unique objectAtIndex:x]]; //add our image to the path
[data writeToFile:fullPath atomically:YES];
}
img = nil;
}
data = nil;
}
However this is not the optimal solution. -dataWithContentsOfURL: is synchronous method and will stop execution of your main thread while downloading the file. As a consequence the UI will hang during the download. To make your UI not to hang you can use asynchronous url requests.
See -sendAsynchronousRequest:queue:completionHandler: method of the NSURLConnection class. Or if your app is only for iOS 7 see -dataTaskWithURL:completionHandler: as alternative.

using GCD but my app is still freezing when loading images from server

I'm essentially loading images from the server every time a user taps the refresh button. However, this loading mechanism freezes the main thread, so I decided to try and use GCD to load the images in a different thread. I've looked at tutorials, and it seems that I'm setting it up right, but I'm still encountering the same issue.
My code:
-(IBAction)refreshButton:(id)sender{
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
[[API sharedInstance] commandWithParams:[NSMutableDictionary dictionaryWithObjectsAndKeys:
#"stream",#"command",#"0",#"IdImage",_contentFilter,#"contentFilter",
nil]
onCompletion:^(NSDictionary *json){
NSMutableArray *tempArray = [json objectForKey:#"result"];
NSMutableArray *tempDataArray = [[NSMutableArray alloc] initWithCapacity:[_imagesTemporaryHolder count]];
NSLog(#"loaded temporary image holder");
for (int i=0;i<[_images count];i++) {
NSLog(#" populating _imagesData");
NSString *imageID = [[tempArray objectAtIndex:i] objectForKey:#"IdImage"];
NSString *imageURL = [NSString stringWithFormat:#"http://swiphtapp.com/Swipht/upload/%#.jpg",imageID];
NSData *tempImageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:imageURL]];
[tempDataArray insertObject:tempImageData atIndex:i];
}
}];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
});
});
}
Could it be that I need to go to the API class and change something there? I'm using AFNNETWORKING by the way.
Thank you so much for the help!
UPDATE 1:
It looks like the API's "onCompletion" block is what's causing the problem...I did the NSLog(isMainThread) check in it, and it returns YES meaning that it's running in the main thread although it shouldn't be as I've encapsulated it in the GCD. Thoughts on how to solve this?
ANSWERED THANKS TO AWESOME PEOPLE
#eric
The completion block of the API call is meant to be performed on the main thread, because it's where you would normally update the views based on the data received. Try placing your GCD stuff INSIDE the completion block instead of encapsulating the whole thing in it. It's taking so long on the main thread because you are getting the NSData from the url returned from your original API call, which can take a long time (relatively speaking)
Basically the GCD needed to be in the API call's completion block.
Thanks again!
I believe this is the answer we came to in the comments under the initial post:
-(IBAction)refreshButton:(id)sender
{
[[API sharedInstance] commandWithParams:[NSMutableDictionary dictionaryWithObjectsAndKeys:
#"stream",#"command",#"0",#"IdImage",_contentFilter,#"contentFilter",
nil]
onCompletion:^(NSDictionary *json){
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
NSMutableArray *tempArray = [json objectForKey:#"result"];
NSMutableArray *tempDataArray = [[NSMutableArray alloc] initWithCapacity:[_imagesTemporaryHolder count]];
NSLog(#"loaded temporary image holder");
for (int i=0;i<[_images count];i++) {
NSLog(#" populating _imagesData");
NSString *imageID = [[tempArray objectAtIndex:i] objectForKey:#"IdImage"];
NSString *imageURL = [NSString stringWithFormat:#"http://swiphtapp.com/Swipht/upload/%#.jpg",imageID];
NSData *tempImageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:imageURL]];
[tempDataArray insertObject:tempImageData atIndex:i];
}
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
});
});
}];
}

Error in downloading an audio file form server synchronously

I am using a NSBlockOperation in which i am trying to downlaod an audio file from server & storing it in documents directory.
NSBlockOperation *audioOperation = [NSBlockOperation blockOperationWithBlock:^{
//Perform doanload
NSString *itemStoredPath = [self downloadPOIAudioByUrl:itemUrl itemName:itemName folderName:itemFolder iteInfo:cacheAudioDetails];
// Update database
.....
}];
-(NSString *)downloadPOIAudioByUrl:(NSString *)itemUrl itemName:(NSString *)itemName folderName:(NSString *)folderName iteInfo:(CacheAudioDetails *)itemInfo {
// Get the url for video upload
NSURL *audioUrl = [NSURL URLWithString:[itemUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
// Set the response parameter
NSURLResponse *serverResponce = nil;
// Set the error parameter
NSError *error = nil;
// Create a request & set the time out interval for 1 min.
//NSURLRequest *videoRequest = [NSURLRequest requestWithURL:videoUrl cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0];
NSURLRequest *audioRequest = [NSURLRequest requestWithURL:audioUrl];
// Set the connection
NSData *audioData = [NSURLConnection sendSynchronousRequest:audioRequest returningResponse:&serverResponce error:&error];
if (error == nil && audioData != nil) {
// Data Found
// Store in directory & return the path to store in database
return audioPath;
}
return nil;
}
I have made a synchronous call to downlaod an audio file. But it is taking too much time & after long time it returns zero bytes of NSData.I thought it was due to my timed out request for 60 sec. Then i removed the time out request but still the problem remains as it is. My query is
Time out is related to server connection & not to fetching data from server
What should be the reason of Zero bytes responce from server.

NSURLConnection sendAsynchronousRequest never free up the memory

I am working on an iOS app which dispatch quite a number of tasks to my serial queue. The task is to download images from my web server, save it to disk, and later displayed on UIImageView. However, [NSURLConnection sendAsynchrousRequest] will keep eating up more and more memory until iOS kill my process.
The downloader method looks like this:
// dispatch_queue_t is created once by: m_pRequestQueue = dispatch_queue_create( "mynamespace.app", DISPATCH_QUEUE_SERIAL);
- (void) downloadImageInBackgroundWithURL:(NSString*) szUrl {
__block typeof(self) bSelf = self;
__block typeof(m_pUrlRequestQueue) bpUrlRequestQueue = m_pRequestQueue;
dispatch_async( m_pRequestQueue, ^{
NSAutoreleasePool *pAutoreleasePool = [[NSAutoreleasePool alloc] init];
NSURLRequest *pRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:szUrl]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:URL_REQUEST_TIMEOUT];
[NSURLConnection sendAsynchronousRequest:pRequest queue:bpUrlRequestQueue completionHandler:^(NSURLResponse *pResponse, NSData *pData, NSError *pError) {
NSAutoreleasePool *pPool = [[NSAutoreleasePool alloc] init];
if ( pError != nil ) {
} else {
// convert image to png format
UIImage *pImg = [UIImage imageWithData:pData];
NSData *pDataPng = UIImagePNGRepresentation(pImg);
bool bSaved = [[NSFileManager defaultManager] createFileAtPath:szCacheFile contents:pDataPng attributes:nil];
}
__block typeof(pDataPng) bpDataPng = pDataPng;
__block typeof(pError) bpError = pError;
dispatch_sync( dispatch_get_main_queue(), ^ {
NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init];
UIImage *pImage = [[UIImage alloc] initWithData:bpDataPng];
// display the image
[pImage release];
// NSLog( #"image retain count: %d", [pImage retainCount] ); // 0, bad access
[autoreleasepool drain];
});
}
[pPool drain];
}]; // end sendAsynchronousRequest
[pAutoreleasePool drain];
}); // end dispatch_async
} // end downloadImageInBackgroundWithURL
I am quite sure it is something inside [NSURLConnection sendAsynchronousRequest] as the profiler is showing that the function is the one eating up all the memory...
However, I am also not very sure about the dispatch_*** and block things, I've always used C and C++ code with pthread before, but after reading from Apple's documentation on migrating away from thread, I decided to give GCD a try, objective-c is so troublesome and I'm not sure how to release the NSData *pData and NSURLResponse *pResponse as it crash whenever I do it.
Please advice... really need help to learn and appreciate objective-c...
ADDITIONAL EDIT:
Thanks to #robhayward, I put the pImg and pDataPng outside as __block variable, use his RHCacheImageView way of downloading data ( NSData initWithContentOfURL )
Thanks as well to #JorisKluivers, the first UIImage can actually be reused to display as UIImageView recognized both jpg and png format, just my later processing requires png format and I am reading from the disk later just when required
I would firstly put it down to the image and data objects that you are creating:
UIImage *pImg = [UIImage imageWithData:pData];
NSData *pDataPng = UIImagePNGRepresentation(pImg);
Which might be hanging around too long, perhaps put them outside the block, as they are probably being created/released on different threads:
__block UIImage *pImg = nil;
__block NSData *pDataPng = nil;
[NSURLConnection sendAsynchronousRequest..
(Also consider using ARC if you can)
I have some code on Github that does a similar job without this issue, feel free to check it out:
https://github.com/robinhayward/RHCache/blob/master/RHCache/RHCache/Helpers/UIImageView/RHCacheImageView.m
First of all try simplifying your code. Things I did:
Remove the outer dispatch_async. This is not needed, your sendAsynchronousRequest is async already. This also removes the need another __block variable on the queue.
You create an image named pImg from the received pData, then convert that back to NSData of type png, and later create another image pImage from that again. Instead of converting over and over, just reuse the first image. You could even write the original pData to disk (unless you really want the png format on disk).
I didn't compile the code below myself, so it might contain a few mistakes. But it is a simpler version that might help solve the leak.
- (void) downloadImageInBackgroundWithURL:(NSString*)szUrl
{
__block typeof(self) bSelf = self;
NSAutoreleasePool *pAutoreleasePool = [[NSAutoreleasePool alloc] init];
NSURLRequest *pRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:szUrl]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:URL_REQUEST_TIMEOUT];
[NSURLConnection sendAsynchronousRequest:pRequest queue:m_pRequestQueue completionHandler:^(NSURLResponse *pResponse, NSData *pData, NSError *pError) {
NSAutoreleasePool *pPool = [[NSAutoreleasePool alloc] init];
if (pError) {
// TODO: handle error
return;
}
// convert image to png format
__block UIImage *pImg = [UIImage imageWithData:pData];
// possibly just write pData to disk
NSData *pDataPng = UIImagePNGRepresentation(pImg);
bool bSaved = [[NSFileManager defaultManager] createFileAtPath:szCacheFile contents:pDataPng attributes:nil];
dispatch_sync( dispatch_get_main_queue(), ^ {
// display the image in var pImg
});
}];
[pAutoreleasePool drain];
}

Resources