I'm using Xcode 4.6.3 and iOS 5.5/6.1.6 .
I am using a background thread to load large quantities of jpg from a server to iOS devices.
dispatch_async(kBgQueue, ^
{
// get the array of filenames to download
NSURL* url = [NSURL URLWithString:webPath];
NSArray* theArray = [NSArray arrayWithContentsOfURL:url];
if( theArray )
{
dispatch_async(dispatch_get_main_queue(), ^{
// disable screen buttons
[self setButtons:false];
});
[self loadImagesFromList:theArray sourceBundle:bundlePath destBundle:localBundlePath manager:manager];
if (!stopFlag) {
// if no memory error has occurred
NSLog(#"calling refresh after load_images");
dispatch_async(dispatch_get_main_queue(), ^{
[self refresh];
});
}
theArray = nil;
}
else
{
NSLog(#"Error loading bundle");
}
});
The background method:
-(void)loadImagesFromList:(NSArray *)theArray
sourceBundle:(NSString *)bundlePath
destBundle:(NSString *)localBundlePath
manager:(NSFileManager *)manager {
// initialize the progress and activity indicator
dispatch_async(dispatch_get_main_queue(), ^{
[self.activityIndictor startAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[self.progressIndictor setProgress:0 animated:NO];
});
NSURL *url;
NSString *srcFile;
NSString *destFile;
NSError *error = nil;
int counter = 0;
float prog = 0;
float increment = 1.0 / [theArray count];
float stepSize = [theArray count] / 10;
for (NSString *file in theArray)
{
if (stopFlag) {
NSLog(#"I see stopFlag = true, counter = %d, prog = %f", counter, prog);
return;
}
srcFile = [bundlePath stringByAppendingPathComponent:file];
destFile = [localBundlePath stringByAppendingPathComponent:file];
counter += 1;
prog += increment;
if (counter == stepSize) {
dispatch_async(dispatch_get_main_queue(), ^{
self.progressIndictor.progress = prog;
});
counter = 0;
}
// only download if file isn't already here
BOOL fileExists = [manager fileExistsAtPath:destFile]; // check if we already have it
if (!fileExists) {
// jpg or folder check
if ([[destFile pathExtension] isEqualToString:#"jpg"]) {
url = [NSURL URLWithString:srcFile];
data = [NSData dataWithContentsOfURL:url
options:0
error:&error];
[data writeToFile:destFile options:NSDataWritingAtomic error:&error];
data = nil;
} else {
[manager createDirectoryAtPath:destFile withIntermediateDirectories:YES attributes:nil error:&error];
}
}
}
}
If the files exist, the loop zips through the array and exits back to the main thread ok.
If any files are missing, the download/write part seems to chew up the RAM and cause low memory warning to trigger. It takes several thousand files to do it.
I've tried declaring the variables outside the loop, and even doing the whole thing in the main thread to test if that was causing the leak.
I tried using the alternate dataWithContentsOfURL:options:error call.
I tried Instruments, but it is really slow and crashes often. Before crashing, it does show allocation going up, up, up slowly.
After several days on this, I'm stumped.
The first thing I'd suggest is using an #autoreleasepool to control the peak amount of memory consumed. Right now, you're downloading the contents into the NSData as an autorelease object, and when done, you're nil-ing that variable, which simple flags it to be deallocated once the autorelease pool is drained (which will not happen until loadImagesFromList is done). By (a) moving the variable declarations inside the for loop; and (b) wrapping this in an #autoreleasepool, your memory will be deallocated as the individual downloads finish.
-(void)loadImagesFromList:(NSArray *)theArray
sourceBundle:(NSString *)bundlePath
destBundle:(NSString *)localBundlePath
manager:(NSFileManager *)manager {
// initialize the progress and activity indicator
dispatch_async(dispatch_get_main_queue(), ^{
// your UI update here
});
int counter = 0;
float prog = 0;
float increment = 1.0 / [theArray count];
float stepSize = [theArray count] / 10;
for (NSString *file in theArray)
{
#autoreleasepool {
if (stopFlag) {
NSLog(#"I see stopFlag = true, counter = %d, prog = %f", counter, prog);
return;
}
NSString *srcFile = [bundlePath stringByAppendingPathComponent:file];
NSString *destFile = [localBundlePath stringByAppendingPathComponent:file];
counter += 1;
prog += increment;
if (counter == stepSize) {
dispatch_async(dispatch_get_main_queue(), ^{
self.progressIndictor.progress = prog;
});
counter = 0;
}
// only download if file isn't already here
BOOL fileExists = [manager fileExistsAtPath:destFile]; // check if we already have it
if (!fileExists) {
NSError *error = nil;
// jpg or folder check
if ([[destFile pathExtension] isEqualToString:#"jpg"]) {
NSURL *url = [NSURL URLWithString:srcFile];
NSData *data = [NSData dataWithContentsOfURL:url
options:0
error:&error];
[data writeToFile:destFile options:NSDataWritingAtomic error:&error];
} else {
[manager createDirectoryAtPath:destFile withIntermediateDirectories:YES attributes:nil error:&error];
}
}
}
}
}
You might want to refactor this code to use NSOperationQueue. This addresses the peak memory issue, but also let's you enjoy a degree of concurrency. Because iOS only allows 4-5 concurrent requests anyway, you want to limit the maximum number of concurrent operations to a reasonable number, and this mitigates network timeout risks if trying to run too many concurrent requests. (This maxConcurrentOperationCount feature is the main reason I suggest using operation queues.)
Anyway, that might look like:
-(void)loadImagesFromList:(NSArray *)theArray
sourceBundle:(NSString *)bundlePath
destBundle:(NSString *)localBundlePath
manager:(NSFileManager *)manager {
// initialize the progress and activity indicator
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// your UI update here
}];
int __block counter = 0;
float __block prog = 0;
float increment = 1.0 / [theArray count];
float stepSize = [theArray count] / 10;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSString *file in theArray)
{
[queue addOperationWithBlock:^{
if (stopFlag) {
NSLog(#"I see stopFlag = true, counter = %d, prog = %f", counter, prog);
return;
}
NSString *srcFile = [bundlePath stringByAppendingPathComponent:file];
NSString *destFile = [localBundlePath stringByAppendingPathComponent:file];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
counter += 1;
prog += increment;
if (counter == stepSize) {
self.progressIndictor.progress = prog;
counter = 0;
}
}];
// only download if file isn't already here
BOOL fileExists = [manager fileExistsAtPath:destFile]; // check if we already have it
if (!fileExists) {
NSError *error = nil;
// jpg or folder check
if ([[destFile pathExtension] isEqualToString:#"jpg"]) {
NSURL *url = [NSURL URLWithString:srcFile];
NSData *data = [NSData dataWithContentsOfURL:url
options:0
error:&error];
[data writeToFile:destFile options:NSDataWritingAtomic error:&error];
} else {
[manager createDirectoryAtPath:destFile withIntermediateDirectories:YES attributes:nil error:&error];
}
}
}];
}
}
There are other refinements I might suggest (e.g. implementing cancelation logic rather than looking at stopFlag), but I was trying to minimize the code changes. I'm just taking advantage of the fact that one can easily replace dispatch_async:
dispatch_async(dispatchQueue, ^{ ... });
with NSOperationQueue method addOperationWithBlock:
[operationQueue addOperationWithBlock:^{ ... }];
But now we can use a concurrent NSOperationQueue with maxConcurrentOperationCount of 4 or 5, and you suddenly enjoy a nice, constrained degree of concurrency. You may find that this is observably faster than downloading files sequentially.
Related
I'm using WKWebView in my app to get data from The HTML with javascript, I'm using this code:
NSString *jsCall = [NSString stringWithFormat:#"xxxxxx('%#');",html];
__block NSString *searchJson = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_sync(dispatch_get_main_queue(), ^{
[self.searchWebView evaluateJavaScript:jsCall completionHandler:^(id result, NSError * _Nullable error) {
if (error || result == nil || ![result isKindOfClass:[NSString class]]) {
searchJson = nil;
} else {
searchJson = result;
}
dispatch_semaphore_signal(semaphore);
}];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if ([NSString stringIsNilOrEmpty:searchJson]) {
return nil;
}
return [self getResultsWithJSonString:searchJson];
I need to wait for the response so i'm using the semaphore_wait method, It's working fine but sometimes the searchJson is released from memory and in the [NSString stringIsNilOrEmpty:searchJson] I'm getting this error:
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000443439e70
Any idea of how to fix this released problem? I'm using Non-ARC in my project.
If it is non-ARC, the you must retain result explicitly (because callback might be in different thread), like
} else {
searchJson = [result retain];
}
and then, to avoid leak, use
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[searchJson autorelease]; // << call it here, once !!
if ([NSString stringIsNilOrEmpty:searchJson]) {
return nil;
}
return [self getResultsWithJSonString:searchJson];
I can't find anything online about threading loading an image from a device and scrolling smoothly through a tableview. There is one on ray wen about this, but it doesn't really help me for my situation.
Does anybody have any advice or code which would help to allow a tableview to scroll smoothly and load images from the device's temporary directory?
i did exactly as mentioned at tutorial, but with modification for nsoperation subclass
this is methods for fetch
-(void) updateData
{
[self.pendingOperations.downloadQueue addOperationWithBlock:^{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSArray *filePathes = [self recursiveRecordsForResourcesOfType:#[#"png", #"jpeg", #"jpg",#"pdf"] inDirectory:documentsDirectory];
#synchronized (self) {
self.documents = filePathes;
NSLog(#"documents count %#", #([self.documents count]));
}
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
[self.delegate modelDidUpdate:self];
});
}];
}
- (NSArray *)recursiveRecordsForResourcesOfType:(NSArray *)types inDirectory:(NSString *)directoryPath{
NSMutableArray *filePaths = [[NSMutableArray alloc] init];
NSMutableDictionary *typesDic = [NSMutableDictionary dictionary];
for (NSString *type in types)
[typesDic setObject:type forKey:type];
// Enumerators are recursive
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:directoryPath];
NSString *filePath;
while ((filePath = [enumerator nextObject]) != nil){
// If we have the right type of file, add it to the list
// Make sure to prepend the directory path
if([typesDic objectForKey:[filePath pathExtension]]){
//[filePaths addObject:[directoryPath stringByAppendingPathComponent:filePath]];
CURFileRecord *record = [CURFileRecord new];
record.filePath =[directoryPath stringByAppendingPathComponent:filePath];
record.fileName = filePath;
[filePaths addObject:record];
}
}
return filePaths;
}
this is .m for subclass
- (void)main {
// 4
#autoreleasepool {
if (self.isCancelled)
return;
NSData *fileData = [[NSFileManager defaultManager] contentsAtPath:self.fileRecord.filePath];
// self.fileRecord.fileData = fileData;
if (self.isCancelled) {
fileData = nil;
return;
}
if (fileData) {
UIImage *newImage;
if ([[self.fileRecord.filePath pathExtension] isEqualToString:#"pdf"])
{
CGPDFDocumentRef doc = [CURDocumentViewerUtilities MyGetPDFDocumentRef:fileData];
newImage = [CURDocumentViewerUtilities buildThumbnailImage:doc withSize:CGSizeMake(64, 96)];
}
else
{
newImage = [CURDocumentViewerUtilities makePreviewImageFromData:fileData];
}
self.fileRecord.previewImage = newImage;
}
else {
self.fileRecord.failed = YES;
}
fileData = nil;
if (self.isCancelled)
return;
// 5
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(imageDownloaderDidFinish:) withObject:self waitUntilDone:NO];
}
}
With update func i've fetched pathes to proccess, and nsoperation subclass loads images. Works fine with 2000 images in fullhd - smoothly and without any lugs
I have downloaded images and saved it to a GCD, updated the count of images and performed a post notification in queue. What is happening now is that it does not register that I have downloaded the images. Sometimes I am missing some images, and I think it is because I have missed something in my GCD logic.
Here is my code:
for (NSString *i in items)
{
[[RequestAPI sharedInstance]downloadImage:i completion:^(AFHTTPRequestOperation *operation, UIImage *image, NSError *error) {
//1. here main thread I receive images and go to BG
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//2. here I save image on disk and get path
NSString *path = [ImageManager saveImageToDisk:image toEntity:entity withparams:#{#"save" : #"lala"}];
__block NSMutableDictionary *attachments = [NSMutableDictionary dictionary];
__block NSMutableArray *photoPaths = [NSMutableArray array];
dispatch_async(dispatch_get_main_queue(), ^{
//3. here I load entity and dictionary from it with NSKeyedUnarchiver from CD and set to it image path
if (entity.attachments)
{
attachments = [NSKeyedUnarchiver unarchiveObjectWithData:entity.attachments];
if (attachments[type])
{
photoPaths = attachments[type];
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//4. here I check all images equality ti themselves in entity
BOOL haveDublicate = NO;
NSData *i = [ImageManager imageDataFromPath:path];
NSArray *photoImages = [ImageManager imageDatasFromPaths:photoPaths];
for (NSData *saved in photoImages)
{
if ([saved isEqualToData: i])
{
haveDublicate = YES;
}
}
if (!photoPaths)
{
photoPaths = [NSMutableArray array];
}
dispatch_async(dispatch_get_main_queue(), ^{
//5. and finally if all ok I save image path, change load counter and post notification
if (path.length
&& ![photoPaths containsObject:path]
&& !haveDublicate
)
{
[photoPaths addObject:path];
[savedLinks setObject:photoPaths forKey:type];
entity.attachments = [NSKeyedArchiver archivedDataWithRootObject:savedLinks];
[self saveContext];
}
[RequestAPI sharedInstance].downloadsCount -= 1;
[[NSNotificationCenter defaultCenter]postNotificationName:kReloadFeedData object:nil];
});
});
});
});
}];
I think here I need process 1-2-3-4-5 to get desired result. Am I right or how can I do this queuing?
Below is the method with the code, where I am getting a memory leak using manual memory management. The memory leak is detected by using Xcode instruments and specifically points to the line where I am using NSJSONSerialization. I am running the target app (on a device with iOS 6.1).
The first time that i tap on the refreshButton there is no leak. Any subsequent tap generates the leak(and more leaks on top of that if i continue tapping the button). Below is the code - This is basic stuff for consuming JSON web services(the web service link is bogus but the real one that I am using works). You will notice that I am using Grand Central Dispatch so that I can update the UI without waiting for the parsing of the JSON to finish.
The line detected by instruments is surrounded by the asterisks. I would like to get some help to anyone who might have an idea of what is going on here. The full stack trace(as mentioned in the below comments i will put here:)
+(NSJSONSerialization JSONObjectWithData:option:error:] -> -[_NSJSONReader parseData:options:] -> -[_NSJSONReader parseUTF8JSONData:skipBytes:options]->newJSONValue->newJSONString->[NSPlaceholderString
initWithBytes:length:encoding:]
-(void)parseDictionary:(NSDictionary *)dictonary{
self.transactions = [dictonary objectForKey:#"transactions"];
if(!self.transactions){
NSLog(#"Expected 'transactions' array");
return;
}
for (int arrayIndex = 0; arrayIndex < [self.transactions count]; arrayIndex++) {
TransactionResult *result = [[[TransactionResult alloc] init] autorelease];
result.transactionID = [[self.transactions objectAtIndex:arrayIndex] objectForKey:#"ID"];
result.transactionDescription = [[self.transactions objectAtIndex:arrayIndex] objectForKey:#"description"];
result.transactionPrice = [[self.transactions objectAtIndex:arrayIndex] objectForKey:#"price"];
self.totalPrice += [result.transactionPrice doubleValue];
NSLog(#"total price: %f", self.totalPrice);
[self.transactionResults addObject:result];
result = nil;
}
}
- (IBAction)refreshButtonPressed:(UIBarButtonItem *)sender {
__block id resultObject;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
NSURL *url = [NSURL URLWithString:#"http://mywebservice.php"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSError *error;
***resultObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];***
if(!error){
if([resultObject isKindOfClass:[NSDictionary class]]){
NSDictionary *dictonary = resultObject;
[self parseDictionary:dictonary];
NSLog(#"Done parsing!");
dispatch_async(dispatch_get_main_queue(), ^{
self.isLoading = NO;
[self.transactionsTableView reloadData];
});
}
else{
NSLog(#"JSON Error: Expected Dictionary");
resultObject = nil;
return;
}
}
else{
NSLog(#"JSON Error: %#", [error localizedDescription]);
dispatch_async(dispatch_get_main_queue(), ^{
resultObject = nil;
[self.transactionsTableView reloadData];
[self showError];
});
return;
}
});
}
I used a ARC as soon as it came out with 4.3, and put an app in the store with it - point being you could switch to ARC. That said, I tried to reproduce your problem by creating a class/file that has the no-arc flag applied to it, but cannot reproduce the problem. This makes me believe your problem is elsewhere. In the code below, I create a Test object in another file, retain it, and send it the test message. No matter what I set "i" to, it always deallocs the object:
#import "Tester.h"
#interface Obj : NSObject <NSObject>
#end
#implementation Obj
- (id)retain
{
NSLog(#"retain");
id i = [super retain];
return i;
}
- (oneway void)release
{
NSLog(#"release");
[super release];
}
- (void)foo
{
}
- (void)dealloc
{
NSLog(#"Obj dealloced");
[super dealloc];
}
#end
#implementation Tester
- (void)test
{
int i = 2;
__block Obj *obj;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
obj = [[Obj new] autorelease];
if(i == 0) {
Obj *o = obj;
dispatch_async(dispatch_get_main_queue(), ^
{
[o foo];
} );
} else if(i == 1) {
obj = nil;
} else if(i == 2) {
dispatch_async(dispatch_get_main_queue(), ^
{
obj = nil;
} );
}
} );
}
#end
I'm trying to use background threds to do some computations on an iPad.
The thing is even thou the computationa are running. The UI is blocked while they run...
What am I doing wrong.
[mc evaluateFormula:adapted runNo:10000];
This is called from an IBAction.
This is the code that is called:
-(void)evaluateFormula:(NSDictionary *)frm runNo:(NSUInteger)runCount
{
self.runCount = runCount;
self.frm = frm;
[self performSelectorInBackground:#selector(backgroundEvalFrm) withObject:nil];
// for (int i = 0; i < runCount; i++) {
// [self runFormula:frm];
// }
//
}
-(void)backgroundEvalFrm
{
percentVal = self.runCount / 100;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:20];
for (int i = 0; i<self.runCount; i++) {
NSInvocationOperation *op =[[NSInvocationOperation alloc] initWithTarget:self selector:#selector(runFormula:) object:self.frm];
[queue addOperation:op];
}
}
So why is the UI blocked?
Here is the thread return code... it's all in the same class
-(void)runFormula:(NSDictionary *)frm
{
NSMutableString *formula = [[frm objectForKey:kFormulaExpresion] mutableCopy];
NSArray *variables = [frm objectForKey:kVariableArray];
NSArray *evals = [self evaluateVariables:variables];
for (NSDictionary *var in evals) {
NSString *sym = [var objectForKey:kVariableSymbol];
[formula replaceOccurrencesOfString:sym withString:[[var objectForKey:#"numVal"] stringValue] options:NSCaseInsensitiveSearch range:NSMakeRange(0, [formula length])];
}
//parse formula
//NSLog(#"formula to parse:%#",formula);
NSNumber *resNo = [formula numberByEvaluatingString];
// NSLog(#"formula %# the result : %f",formula,[resNo doubleValue]);
//NSNumber *resNo = [NSNumber numberWithDouble:result];
[self performSelectorOnMainThread:#selector(addNewResult:) withObject:resNo waitUntilDone:NO];
}
#pragma mark -- data aggregation delegate
-(void)addNewResult:(NSNumber *)nr
{
NSLog(#"index : %i result: %f",currentIndex,[nr doubleValue]);
[[self delegate] didReceiveResult:nr];
resultsArray[currentIndex]=[nr doubleValue];
currentIndex ++;
if ( (currentIndex % percentVal) == 0) {
[[self delegate] percentCompleted];
}
}
if your calculations are all competing for the same resource (i.e. CPU or I/O) in an uncoordinated manner (highly probable), then you should significantly lower the maximum concurrent operation count -- try 2. chances are, they will complete using less time/energy. furthermore, the main thread will not be reduced to less than 5% of the CPU time during the period that the calculations are executing (result: more responsive UI).