Big image download loop keeps leaking inside GCD queue - objective-c

I'm downloading over 6000 high resolution (about 2.6GB), but can change depending on which customer logs in into our app. Because the sales agents need all the product images to be loaded when they are outside the country they need all to be available in offline-mode.
My code is pretty much ad-hoc and the problem of my code leaking is probably very obvious to the more experienced Objective-C programmers.
At the moment the status is that the code has downloaded 2041 of the 6253 and 580 seconds have passed. Yet the emulator's memory is already at 931MB.
Could I do anything to improve this code to get the memory back after each loop or X-amount of loop cycles?
int total_new_images = total_images_in_db - total_images_locally;
// Fetch the real pictures now
dispatch_queue_t imageQueue = dispatch_queue_create("Image Queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(imageQueue, ^{
NSString *path = [NSString new];
NSURL *url = [NSURL new];
NSData *data = [NSData new];
NSString *savePath = [NSString new];
NSArray *imgUrlArray = [NSArray new];
NSDate *starTime = [NSDate new];
#autoreleasepool {
int c = 0;
FMResultSet *rs = [self.efashionDB executeQuery:#"SELECT * FROM app_images"];
while ([rs next]) {
imgUrlArray = [[rs stringForColumn:#"full_url"] componentsSeparatedByString:#"/"];
savePath = [NSString stringWithFormat:#"%#/%#", imgFolder, [imgUrlArray objectAtIndex:[imgUrlArray count]-1]];
if ([self.fileMgr fileExistsAtPath:savePath]) {
//NSLog(#"'%#' is already saved locally.", [imgUrlArray objectAtIndex:[imgUrlArray count]-1]);
continue;
}
path = [NSString stringWithString:[rs stringForColumn:#"full_url"]];
url = [NSURL URLWithString:path];
data = [NSData dataWithContentsOfURL:url];
if (![data writeToFile:savePath atomically:YES]) {
NSLog(#"Saving of \"%#\" failed!", [imgUrlArray objectAtIndex:[imgUrlArray count]-1]);
} else {
//NSLog(#"Saving of \"%#\" succeeded!", [imgUrlArray objectAtIndex:[imgUrlArray count]-1]);
// Escape back to the main thread and give status update
dispatch_async(dispatch_get_main_queue(), ^{
NSString *progress = [[NSString alloc] initWithFormat:#"%d of the %d new images downloaded\nTime passed: %d seconds", c, total_new_images, (int)([starTime timeIntervalSinceNow] * -1)];
[_imageConsole setText:progress];
});
c++; // Only increment if it succeeded
}
if (c == 20) {
//break;
}
}
[rs close];
}
});

Try moving the #autoreleasepool inside the while loop. Otherwise your allocated objects won't be released until the while loop has finished.

Related

TableView doesn't show uiimage

I have an app that shows twitter account feed. So I have ImageView, textLabel and detailLabel for the content of the feed. The problem is that when all the data is loaded, the uiimage doesn't appear. When I click on the cell or scroll up-down, images are set. here is some of my code.
-(void)getImageFromUrl:(NSString*)imageUrl asynchronouslyForImageView:(UIImageView*)imageView andKey:(NSString*)key{
dispatch_async(dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:imageUrl];
__block NSData *imageData;
dispatch_sync(dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
imageData =[NSData dataWithContentsOfURL:url];
if(imageData){
[self.imagesDictionary setObject:[UIImage imageWithData:imageData] forKey:key];
dispatch_sync(dispatch_get_main_queue(), ^{
imageView.image = self.imagesDictionary[key];
});
}
});
});
}
- (void)refreshTwitterHomeFeedWithCompletion {
// Request access to the Twitter accounts
ACAccountStore *accountStore = [[ACAccountStore alloc] init];
ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
[accountStore requestAccessToAccountsWithType:accountType options:nil completion:^(BOOL granted, NSError *error){
if (granted) {
NSArray *accounts = [accountStore accountsWithAccountType:accountType];
// Check if the users has setup at least one Twitter account
if (accounts.count > 0)
{
ACAccount *twitterAccount = [accounts objectAtIndex:0];
NSLog(#"request.account ...%#",twitterAccount.username);
NSURL* url = [NSURL URLWithString:#"https://api.twitter.com/1.1/statuses/home_timeline.json"];
NSDictionary* params = #{#"count" : #"50", #"screen_name" : twitterAccount.username};
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:url parameters:params];
request.account = twitterAccount;
[request performRequestWithHandler:^(NSData *responseData,
NSHTTPURLResponse *urlResponse, NSError *error) {
if (error)
{
NSString* errorMessage = [NSString stringWithFormat:#"There was an error reading your Twitter feed. %#",
[error localizedDescription]];
NSLog(#"%#",errorMessage);
}
else
{
NSError *jsonError;
NSArray *responseJSON = [NSJSONSerialization
JSONObjectWithData:responseData
options:NSJSONReadingAllowFragments
error:&jsonError];
if (jsonError)
{
NSString* errorMessage = [NSString stringWithFormat:#"There was an error reading your Twitter feed. %#",
[jsonError localizedDescription]];
NSLog(#"%#",errorMessage);
}
else
{
NSLog(#"Home responseJSON..%#",(NSDictionary*)responseJSON.description);
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData:responseJSON];
});
}
}
}];
}
}
}];
}
-(void)reloadData:(NSArray*)jsonResponse
{
self.tweets = jsonResponse;
[self.tableView reloadData];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return self.tweets.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
SNTwitterCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(!cell)
{
cell = [[SNTwitterCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
NSDictionary *tweetDictionary = self.tweets[indexPath.row];
NSDictionary *user = tweetDictionary[#"user"];
NSString *userName = user[#"name"];
NSString *tweetContaint = tweetDictionary[#"text"];
NSString* imageUrl = [user objectForKey:#"profile_image_url"];
[self getImageFromUrl:imageUrl asynchronouslyForImageView:cell.imageView andKey:userName];
cell.profileImage.image = [UIImage imageNamed:#"images.png"];
NSArray *days = [NSArray arrayWithObjects:#"Mon ", #"Tue ", #"Wed ", #"Thu ", #"Fri ", #"Sat ", #"Sun ", nil];
NSArray *calendarMonths = [NSArray arrayWithObjects:#"Jan", #"Feb", #"Mar",#"Apr", #"May", #"Jun", #"Jul", #"Aug", #"Sep", #"Oct", #"Nov", #"Dec", nil];
NSString *dateStr = [tweetDictionary objectForKey:#"created_at"];
for (NSString *day in days) {
if ([dateStr rangeOfString:day].location == 0) {
dateStr = [dateStr stringByReplacingOccurrencesOfString:day withString:#""];
break;
}
}
NSArray *dateArray = [dateStr componentsSeparatedByString:#" "];
NSArray *hourArray = [[dateArray objectAtIndex:2] componentsSeparatedByString:#":"];
NSDateComponents *components = [[NSDateComponents alloc] init];
NSString *aux = [dateArray objectAtIndex:0];
int month = 0;
for (NSString *m in calendarMonths) {
month++;
if ([m isEqualToString:aux]) {
break;
}
}
components.month = month;
components.day = [[dateArray objectAtIndex:1] intValue];
components.hour = [[hourArray objectAtIndex:0] intValue];
components.minute = [[hourArray objectAtIndex:1] intValue];
components.second = [[hourArray objectAtIndex:2] intValue];
components.year = [[dateArray objectAtIndex:4] intValue];
NSTimeZone *gmt = [NSTimeZone timeZoneForSecondsFromGMT:2];
[components setTimeZone:gmt];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
[calendar setTimeZone:[NSTimeZone systemTimeZone]];
NSDate *date = [calendar dateFromComponents:components];
NSString *tweetDate = [self getTimeAsString:date];
NSString *tweetValues = [NSString stringWithFormat:#"%# :%#",userName,tweetDate];
cell.textLabel.text = [NSString stringWithFormat:#"%#",tweetValues];
cell.detailTextLabel.text = [NSString stringWithFormat:#"%#",tweetContaint];
[cell.detailTextLabel setFont:[UIFont fontWithName:#"Helvetica" size:20]];
return cell;
}
- (NSString*)getTimeAsString:(NSDate *)lastDate {
NSTimeInterval dateDiff = [[NSDate date] timeIntervalSinceDate:lastDate];
int nrSeconds = dateDiff;//components.second;
int nrMinutes = nrSeconds / 60;
int nrHours = nrSeconds / 3600;
int nrDays = dateDiff / 86400; //components.day;
NSString *time;
if (nrDays > 5){
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateStyle:NSDateFormatterShortStyle];
[dateFormat setTimeStyle:NSDateFormatterNoStyle];
time = [NSString stringWithFormat:#"%#", [dateFormat stringFromDate:lastDate]];
} else {
// days=1-5
if (nrDays > 0) {
if (nrDays == 1) {
time = #"1 day ago";
} else {
time = [NSString stringWithFormat:#"%d days ago", nrDays];
}
} else {
if (nrHours == 0) {
if (nrMinutes < 2) {
time = #"just now";
} else {
time = [NSString stringWithFormat:#"%d minutes ago", nrMinutes];
}
} else { // days=0 hours!=0
if (nrHours == 1) {
time = #"1 hour ago";
} else {
time = [NSString stringWithFormat:#"%d hours ago", nrHours];
}
}
}
}
return [NSString stringWithFormat:NSLocalizedString(#"%#", #"label"), time];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 100;
}
The fundamental problem is that the standard imageView property of the standard table view cell will automatically resize itself based upon the image that is present when cellForRowAtIndexPath finishes. But since there is no image yet when you first present the table, the cell is laid out as if there's no image. And when you asynchronously update the image view's image, it won't resize the image view.
There are a couple of ways of solving this:
Don't use the default imageView provided by UITableViewCell, but rather define your own custom cell subclass with an IBOutlet to its own UIImageView property. Make sure that this UIImageView has a fixed layout (i.e., it doesn't use the intrinsic size derived from the underlying image).
If you do that, you can asynchronously update the image property for your custom UIImageView outlet, and because the layout was not contingent upon the presence of the image, any asynchronous updates of that image should appear correctly.
When you receive the image, don't just set the image view's image property, but rather reload the whole row associated with that NSIndexPath using reloadRowsAtIndexPaths.
If you do this, the cell will be laid out correctly assuming that you retrieve the image from the cache correctly, and do so before cellForRowAtIndexPath finishes.
Note, if you do this, you will need to fix your getImageFromUrl to actually try to retrieve the image from the cache first (and do this from the main queue, before to dispatch to the background queue), or else you'll end up in an endless loop.
Having said that, there are deeper problems here.
As I mentioned above, you're caching your images, but never using the cache when retrieving the images.
You are asynchronously updating the image view.
You should initialize the image property of the UIImageView before you initiate the new asynchronous fetch, otherwise when a cell is reused, you'll see the old image there until the new image is retrieved.
What if the cell was reused in the intervening period between calling getImageFromUrl and when the asynchronous request finishes? You'll be updating the image view for the wrong cell. (This problem will be more apparent when doing this over a slow connection. Run your code using the network link conditioner to simulate slow connections and you'll see the problem I'm describing.)
What if the user rapidly scrolls down to the 100th row in the table? The network requests for the visible cells will be backlogged behind the other 99 image requests. You could even get timeout errors on slow connections.
There are a bunch of tactical little issues in getImageFromUrl.
Why dispatching synchronously from global queue to another global queue? That's unnecessary. Why dispatching UI update synchronously to main thread? That's inefficient.
Why define imageData as __block outside of the block; just define it within the block and you don't need __block qualifier.
What if you didn't receive a valid UIImage from the network request (e.g. you got a 404 error message); the existing code would crash. There are all sorts of responses the server might provide which are not a valid image, and you really must identify that situation (i.e. make sure that not only was NSData you received not nil, but also that the UIImage that you created from it was not nil, too).
I'd probably use NSCache rather than NSMutableDictionary for the cache. Also, regardless of whether you use NSCache or NSMutableDictionary, you want to make sure that you respond to memory pressure events and empty that cache if needed.
We can go through all of these individual problems, but it's a non-trivial amount of work to fix all of this. I might therefore suggest you consider the UIImageView categories of SDWebImage or AFNetworking. They take care of most of these issues, plus others. It will make your life much, much easier.

iOS 7 NSURLSession Download multiple files in Background

I want to download a List of files using NSUrlSession.
I have a variable for counting the successful downloads #property (nonatomic) int downloadsSuccessfulCounter;. While the files are being downloaded I disable the Download Button. When the counter is equal to the download list size, I enable the button again and set the counter to 0. I do this in the method:
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
...
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
downloadsSuccessfulCounter++;
if(downloadsSuccessfulCounter == self.downloadList.count) {
NSLog(#"All downloads finished");
[self.syncButton setEnabled:YES];
downloadsSuccessfulCounter = 0;
}
}];
}
Everything is working fine, but when I open again the ViewController I get the message A background URLSession with identifier com.myApp already exists!. The counter is not set to 0 and the UI elements (UIButtons, UILabels) are not responding.
I guess the problem is because the NSURLSession is still open but I'm not really sure about how it works.
I have tried all the tutorials, but 99% of them are only for downloading 1 file, not more than 1...
Any ideas?
Here is my code:
...
#property (nonatomic, strong) NSURLSession *session;
...
- (void)viewDidLoad {
[super viewDidLoad];
appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.downloadList = [[NSMutableArray alloc] init];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.myApp"];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 5;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
}
When I press the Download ButtonI call this method (
I have a Downloadable object which contains a NSURLSessionDownloadTask):
-(void)startDownload {
for (int i=0; i<[self.downloadList count]; i++) {
Downloadable *d = [self.downloadList objectAtIndex:i];
if (!d.isDownloading) {
if (d.taskIdentifier == -1) {
d.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:d.downloadSource]];
}else {
d.downloadTask = [self.session downloadTaskWithResumeData:fdi.taskResumeData];
}
d.taskIdentifier = d.downloadTask.taskIdentifier;
[d.downloadTask resume];
d.isDownloading = YES;
}
}
}
When the app is in Background:
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([downloadTasks count] == 0) {
if (appDelegate.backgroundTransferCompletionHandler != nil) {
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
appDelegate.backgroundTransferCompletionHandler = nil;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completionHandler();
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertBody = #"All files downloaded";
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
}];
}
}
}];
}
So, as I mentioned in my comments, the issue is that each File requires a unique NSURLSession, and each NSURLSession requires a NSURLSessionConfiguration with a unique identifier.
I think that you were close - and probably more proper than me in certain aspects...
You just need to create a structure to pass unique IDs into unique Configurations, to populate unique Sessions (say that 10x fast).
Here's what I did:
/*
* Retrieves the List of Files to Download
* Also uses the size of that list to instantiate items
* In my case, I load a character returned text file with the names of the files that I want to download
*/
- (void) getMediaList {
NSString *list = #"http://myserver/media_list.txt";
NSURLSession *session = [NSURLSession sharedSession]; // <-- BASIC session
[[session dataTaskWithURL:[NSURL URLWithString:list]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSString *stringFromData = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
// Populate Arrays
REMOTE_MEDIA_FILE_PATHS = [stringFromData componentsSeparatedByString:#"\n"];
[self instantiateURLSessions:[REMOTE_MEDIA_FILE_PATHS count]];
// Start First File
[self getFile:[REMOTE_MEDIA_FILE_PATHS objectAtIndex:downloadCounter]:downloadCounter]; // this variable is 0 at the start
}]
resume];
}
/*
* This sets Arrays of Configurations and Sessions to the proper size
* It also gives a unique ID to each one
*/
- (void) instantiateURLSessions : (int) size {
NSMutableArray *configurations = [NSMutableArray array];
NSMutableArray *sessions = [NSMutableArray array];
for (int i = 0; i < size; i++) {
NSString *index = [NSString stringWithFormat:#"%i", i];
NSString *UniqueIdentifier = #"MyAppBackgroundSessionIdentifier_";
UniqueIdentifier = [UniqueIdentifier stringByAppendingString:index];
[configurations addObject: [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:UniqueIdentifier]];
[sessions addObject:[NSURLSession sessionWithConfiguration: [configurations objectAtIndex:i] delegate: self delegateQueue: [NSOperationQueue mainQueue]]];
}
NSURL_BACKGROUND_CONFIGURATIONS = [NSArray arrayWithArray:configurations];
NSURL_BACKGROUND_SESSIONS = [NSArray arrayWithArray:sessions];
}
/*
* This sets up the Download task for each file, based off of the index of the array
* It also concatenates the path to the actual file
*/
- (void) getFile : (NSString*) file :(int) index {
NSString *fullPathToFile = REMOTE_MEDIA_PATH; // Path To Server With Files
fullPathToFile = [fullPathToFile stringByAppendingString:file];
NSURL *url = [NSURL URLWithString:fullPathToFile];
NSURLSessionDownloadTask *downloadTask = [[NSURL_BACKGROUND_SESSIONS objectAtIndex:index ] downloadTaskWithURL: url];
[downloadTask resume];
}
/*
* Finally, in my delegate method, upon the completion of the download (after the file is moved from the temp data), I check if I am done and if not call the getFiles method again with the updated counter for the index
*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
// Get the documents directory URL
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:LOCAL_MEDIA_PATH];
NSURL *customDirectory = [NSURL fileURLWithPath:dataPath];
// Get the file name and create a destination URL
NSString *sendingFileName = [downloadTask.originalRequest.URL lastPathComponent];
NSURL *destinationUrl = [customDirectory URLByAppendingPathComponent:sendingFileName];
// Move the file
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager moveItemAtURL:location toURL:destinationUrl error: &error]) {
// List
[self listCustomDirectory];
if(downloadCounter < [REMOTE_MEDIA_FILE_PATHS count] -1) {
// Increment Counter
downloadCounter++;
// Start Next File
[self getFile:[REMOTE_MEDIA_FILE_PATHS objectAtIndex:downloadCounter]:downloadCounter];
}
else {
// FINISH YOUR OPERATION / NOTIFY USER / ETC
}
}
else {
NSLog(#"Damn. Error %#", error);
// Do Something Intelligent Here
}
}

How to get array of images from network faster? (iOS)

Basically, I have an array of urls as strings, and as I loop through this array, if the element is a url for an image, I want to turn that url into a UIImage object and add it to another array. This is very slow though since I have to request the data for each URL. I've tried using dispatch_async as I show below but it doesn't seem to make any difference at all.
The key is that when I add these objects to my other array, whether they are images or something else they have to stay in order. Can anyone offer any guidance?
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i=0; i<[slides count]; i++){
__block NSString *mediaURLString = [primaryPhoto objectForKey:#"url"];
if ([self mediaIsVideo:mediaURLString]){
***some code***
}
else{ //if media is an image
dispatch_group_async(group, queue, ^{
mediaURLString = [mediaURLString stringByAppendingString:#"?w=1285&h=750&q=150"];
NSURL *url = [NSURL URLWithString:mediaURLString];
[mutableMedia addObject:url];
NSURL *url = ((NSURL *)self.mediaItem);
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLResponse *response;
NSError *error;
NSData *urlData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
UIImage *image = [[UIImage alloc] initWithData:urlData];
[mutableMedia replaceObjectAtIndex:i withObject:image];
});
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
try this:
[self performSelectorInBackground:#selector(WebServiceCallMethod) withObject:nil];
and create one method like this
-(void)WebServiceCallMethod
{
mediaURLString = [mediaURLString stringByAppendingString:#"?w=1285&h=750&q=150"];
NSURL *url = [NSURL URLWithString:mediaURLString];
[mutableMedia addObject:url];
NSURL *url = ((NSURL *)self.mediaItem);
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLResponse *response;
NSError *error;
NSData *urlData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
UIImage *image = [[UIImage alloc] initWithData:urlData];
[mutableMedia replaceObjectAtIndex:i withObject:image];
}
Hope it Helps!!
Do yourself a favor and don't use +sendSynchronousRequest:... Try something like this instead:
dispatch_group_t group = dispatch_group_create();
for (int i=0; i<[slides count]; i++)
{
__block NSString *mediaURLString = [primaryPhoto objectForKey:#"url"];
if ([self mediaIsVideo:mediaURLString]){
***some code***
}
else
{
//if media is an image
mediaURLString = [mediaURLString stringByAppendingString:#"?w=1285&h=750&q=150"];
NSURL *url = [NSURL URLWithString:mediaURLString];
[mutableMedia addObject:url];
NSURL *url = ((NSURL *)self.mediaItem);
NSURLRequest *request = [NSURLRequest requestWithURL:url];
dispatch_group_enter(group);
[NSURLConnection sendAsynchronousRequest: request queue: [NSOperationQueue mainQueue] completionHandler:
^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (data.length && nil == connectionError)
{
UIImage *image = [[UIImage alloc] initWithData:data];
[mutableMedia replaceObjectAtIndex:i withObject:image];
}
dispatch_group_leave(group);
}];
}
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// Do stuff here that you want to have happen after all the images are loaded.
});
This will start asynchronous requests for all your URLs. When each request finishes, it will run its completion handler which will update your array, and when all requests have finished, the block in the dispatch_group_notify call will be executed.
This approach has the advantage that you can call it from the main thread, all individual completion blocks will be run on the main thread (thus ensuring thread-safety for the mutableMedia array (at least as far as this code goes)) and the final completion block will also be run on the main thread, so you can do whatever you need to update the UI directly.
There is a nifty solution using dispatch lib. The code below should stand for itself.
The basic idea is that an array contains "input" objects which each will be "transformed" via an asynchronous unary task - one after the other. The final result of the whole operation is an array of the transformed objects.
Everything here is asynchronous. Every eventual result will be passed in a completion handler which is a block where the result is passed as a parameter to the call-site:
typedef void (^completion_t)(id result);
The asynchronous transform function is a block which takes the input as a parameter and returns a new object - via a completion handler:
typedef void (^unary_async_t)(id input, completion_t completion);
Now, the function transformEach takes the input values as an NSArray parameter inArray, the transform block as parameter transform and the completion handler block as parameter completion:
static void transformEach(NSArray* inArray, unary_async_t transform, completion_t completion);
The implementation is a follows:
static void do_each(NSEnumerator* iter, unary_async_t transform,
NSMutableArray* outArray, completion_t completion)
{
id obj = [iter nextObject];
if (obj == nil) {
if (completion)
completion([outArray copy]);
return;
}
transform(obj, ^(id result){
[outArray addObject:result];
do_each(iter, transform, outArray, completion);
});
}
static void transformEach(NSArray* inArray, unary_async_t transform,
completion_t completion) {
NSMutableArray* outArray = [[NSMutableArray alloc] initWithCapacity:[inArray count]];
NSEnumerator* iter = [inArray objectEnumerator];
do_each(iter, transform, outArray, completion);
}
And build and run the following example
int main(int argc, const char * argv[])
{
#autoreleasepool {
// Example transform:
unary_async_t capitalize = ^(id input, completion_t completion) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
if ([input respondsToSelector:#selector(capitalizedString)]) {
NSLog(#"processing: %#", input);
NSString* result = [input capitalizedString];
if (completion)
completion(result);
}
});
};
transformEach(#[#"a", #"b", #"c"], capitalize, ^(id result){
NSLog(#"Result: %#", result);
});
sleep(10);
}
return 0;
}
will print this to the console:
2013-07-31 15:52:49.786 Sample2[1651:1603] processing: a
2013-07-31 15:52:50.789 Sample2[1651:1603] processing: b
2013-07-31 15:52:51.792 Sample2[1651:1603] processing: c
2013-07-31 15:52:51.793 Sample2[1651:1603] Result: (
A,
B,
C
)
You can easily create a category for NSArray which implements, say a
-(void) asyncTransformEachWithTransform:(unary_async_t)transform
completion:(completion_t)completionHandler;
method.
Have fun! ;)
Edit:
IFF you ask how this applies to your problem:
The array of URLs is the input array. In order to create the transform block, simply wrap your asynchronous network request in an asynchronous method, say:
`-(void) fetchImageWithURL:(NSURL*)url completion:(completion_t)completionHandler;`
Then wrap method fetchImageWithURL:completion: into a appropriate transform block:
unary_async_t fetchImage = ^(id url, completion_t completion) {
[self fetchImageWithURL:url completion:^(id image){
if (completion)
completion(image); // return result of fetch request
}];
};
Then, somewhere in your code (possible a view controller) assuming you implemented the category for NSArray, and your array of urls is property urls:
// get the images
[self.urls asyncTransformEachWithTransform:fetchImage completion:^(id arrayOfImages) {
// do something with the array of images
}];

Memory leak issue while downloading large number of images

I am trying to downloading more then 600 images in loop with a progress meter on the top of the screen to the user. I blocked my screen with a fade layer for showing activity and progress.
I am getting the memory warning message in between and app getting crashes.
My steps to reach the loop are :
On app delegate, I check first core data table for all rows which is having "0" value in isImageAvailable bool field.
If shows me some count (say 600), and I show and alert with YES and NO option.
On YES : [self performSelector:#selector(myDownload:) withObject:nil afterDelay:0.2];
in myDownload
NSOperationQueue *queue = [NSOperationQueue new];
// Create our NSInvocationOperation to call loadDataWithOperation, passing in nil
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(startUpdatingRecords:) object:nil];
// Add the operation to the queue
[queue addOperation:operation];
[operation release];
[queue release];
in startUpdatingRecords :
-(void)startUpdatingRecords:(id)sender
{
[self performSelectorInBackground:#selector(updateProgressMeter:) withObject: [NSString stringWithFormat:#"%d",self.loopStartIndex]];
// Variable declarations
CGSize newSizeLarge ;
NSPredicate *predicate;
NSMutableArray *MatchingID;
Image_DB *data;
// Cache Directory path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSData *responseData; // = [[NSData alloc]init] ;
NSURL *url = [[[NSURL alloc]init] autorelease];
NSMutableURLRequest *request = [[[NSMutableURLRequest alloc]init] autorelease];
UIImage *imgSelected_Large = [[[UIImage alloc]init] autorelease];
// Loop through all IDs
for (int i = 0; i < [self.arrayOfID count]; i++) //for (int i = loopStart; i < loopEnd; i++)
{
if (self.abortDownload)
{
break;
}
NSString *documentsDirectory = [[[NSString alloc] initWithFormat:#"%#",[paths objectAtIndex:0]] autorelease];
documentsDirectory = [paths objectAtIndex:0];
documentsDirectory = [documentsDirectory stringByAppendingFormat:#"/ImageFolder"]; // Image folder path
myClass *classObj = [self.arrayOfID objectAtIndex:i];
NSString *strURl = [[[NSString alloc] initWithFormat:#"%#%#", self.MyURL,recipeObj.recipeImageStr] autorelease];
//NSLog(#"URL = %#",strURl);
url = [NSURL URLWithString:strURl];
request = [NSMutableURLRequest requestWithURL:url];
responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:NULL]; // Get Image Data into NSData
//imgSelected_Large = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:strURl]]];
NSLog(#"Download Count = %d",i+1);
if (responseData != nil)
{
imgSelected_Large = [UIImage imageWithData:responseData];
// Resizining image
newSizeLarge.width = 320;
newSizeLarge.height = 180;
imgSelected_Large = [self imageWithImage:imgSelected_Large scaledToSize:newSizeLarge]; // New sized image
NSData *dataPhoto; // no need to release it because UIImageJPEGRepresentation gives autoreleased NSData obj.
dataPhoto = UIImageJPEGRepresentation(imgSelected_Large, 0.6); // Set new image representation and its Compression Quality
documentsDirectory = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"Image_%d", classObj.nodeID]];
[dataPhoto writeToFile:documentsDirectory atomically:YES]; //Write file to local folder at default path
predicate = [NSPredicate predicateWithFormat: #"(image_ID = %d )",recipeObj.nodeID];
MatchingID = [CoreDataAPIMethods searchObjectsInContext:#"Image_DB" :predicate :#"image_ID" :YES :self.managedObjectContext];
// Setting flag variable for available image
for (int j = 0; j< [MatchingID count]; j++)
{
//Assign the Authors Records in Class Object and save to Database
data = (Image_DB*) [MatchingID objectAtIndex:j];
// data.image_large = dataPhoto; // Code for storing BLOB object to DB
data.extra_1 = #"1";
//NSLog(#"Flag updated");
}
}
// Exit out code
if ( i == [self.arrayOfID count] - 1 || i == [self.arrayOfID count]) // Its the last record to be stored
{
NSError *error;
if (![self.managedObjectContext save:&error])
{
// Handle the error...
NSLog(#"Error in updating %#",error);
}
self.isUpdateImageCalled = NO;
[self performSelectorOnMainThread:#selector(removeProgressMeter) withObject:nil waitUntilDone:NO];
}
// Update UI screen while in downloading process
[self performSelectorInBackground:#selector(updateProgressMeter:) withObject:[NSString stringWithFormat:#"%d",self.loopStartIndex+i+1]];
}
}
If I didn't release responseData then my app shows me memory warning and got crashed. If I released then, [NSConcreteMutableData release]: message sent to deallocated instance 0x1e931de0 error occures.
How to refine my code. Can any one suggest me on my code and rework and make a refined code.
Please please help me out.
Your responseData returned by sendSynchronousRequest is autoreleased thus you shouldn't release it yourself. For the first sight I don't see a memory leak in your code. It is possible that your application actually uses too much memory, without leaking it. Try to place an autorelease pool inside your for cycle:
for (...) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// your original code with a lot of autoreleased objects
[pool release];
}
If you wrap your code within an autorelease pool, all objects that are sent the autorelease message inside the wrap will be actually released when the pool itself is released: this way you purge the memory in every for cycle.
See also Using Autorelease Pools in the doc, it specifically mentions that you should use them in the case "if you write a loop that creates many temporary objects".

Mysterious Memory Leak - NSString Autorelease Issue

Even though I'm using NSAutoreleasePool in a tight loop, the following line in the method below is causing me to get memory warnings and ultimately crashing my app (by commenting out that line, the problem goes away). Anyone have an idea why this is the case?
filepath = [docpath stringByAppendingPathComponent:file];
-(void)fileCleanup
{
NSString *documentspath = [AppSession documentsDirectory];
NSString *docpath = [documentspath stringByAppendingPathComponent:#"docs"];
NSFileManager *filemanager = [NSFileManager defaultManager];
NSArray *files = [filemanager subpathsOfDirectoryAtPath:docpath error:NULL];
NSLog(#"fileCleanup");
if(files != nil && [files count] > 0)
{
BOOL deletefile;
NSString *filepath;
NSAutoreleasePool *readPool;
NSString *testfile;
NSString *file;
for(file in files)
{
deletefile = YES;
for (testfile in allFiles) {
readPool = [[NSAutoreleasePool alloc] init];
//line below is causing memory leak!
filepath = [docpath stringByAppendingPathComponent:file];
//if([filepath isEqualToString:testfile])
//{
// deletefile = NO;
// break;
//}
[readPool drain];
}
if(deletefile)
{
[self logText:[#"\nD: " stringByAppendingString:[#"docs/" stringByAppendingPathComponent:file]]];
[filemanager removeItemAtPath:[docpath stringByAppendingPathComponent:file] error:NULL];
}
}
}
I replaced the inner "for" loop with the Objective C equivalent of the php in_array() function and the memory issues disappeared!
NSUInteger matchint = [allFiles indexOfObject:[docpath stringByAppendingPathComponent:file]];
if(matchint != NSNotFound)
deletefile = NO;
The break statement was dropping you out of your inner for loop without calling -drain on the NSAutoreleasePool.