Two Methods of Getting an Image from URL: The Differences? - objective-c

What are the main differences between the following two methods of fetching a UIImage from a URL? I recently switched from Method 1 to Method 2 in my app and seemed to experience a drastic increase in speed when I thought that, essentially, both methods were nearly the same in practice. Just trying to figure out why I saw such a speed increase.
Method 1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:self.imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
self.image = [UIImage imageWithData:imageData];
});
});
Method 2
- (void)fetchImage
{
NSURLRequest *request = [NSURLRequest requestWithURL:self.imageURL];
self.imageData = [[NSMutableData alloc] init];
self.imageURLConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
if(connection == self.imageURLConnection)
{
[self.imageData appendData:data];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if(connection == self.imageURLConnection)
{
self.image = [UIImage imageWithData:self.imageData];
}
}

My best guess is that because for Method 1 the AsyncURLConnection class multithreads:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/* process downloaded data in Concurrent Queue */
dispatch_async(dispatch_get_main_queue(), ^{
/* update UI on Main Thread */
So, you may see degraded performance due to contention for shared resources.
On the other hand Method 2, is actually just a collection of methods which are implemented more like transaction processing.
There's probably also more to it.

Related

Data transfer between NSOperations

I would like to obtain the following: I have two NSOperations in a NSOperationQueue. The firs is a download from a website (gets some json data) the next is parsing that data. This are dependent operations.
I don't understand how to link them together. If they are both allocated and in the queue, how do I transfer the json string to the operation that parses it? Is it a problem if this queue is inside another NSOperationQueue that executes an NSOperation that consists of the two mentioned previously?
All I could find is transfers of data to a delegate on the main thread (performSelectorOnMainThread), but I need all this operations to execute in the background.
Thanks.
Code:
NSDownload : NSOperation
- (instancetype)initWithURLString:(NSString *)urlString andDelegate:(id<JSONDataDelegate>)delegate
{
self = [super init];
if (self) {
_urlStr = urlString;
_delegate = delegate; /// this needs to be a NSOPeration
_receivedData = [NSMutableData dataWithCapacity:256];
}
return self;
}
#pragma mark - OVERRIDE
- (void)main
{
#autoreleasepool {
if (self.isCancelled) {
return;
}
NSURL *url = [NSURL URLWithString:self.urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
self.urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
}
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
if (self.isCancelled) {
[connection cancel];
self.receivedData = nil;
return;
}
[self.receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if (self.isCancelled) {
self.receivedData = nil;
return;
}
// return data to the delegate
NSDictionary *responseDict = #{JSON_REQUESTED_URL : self.urlStr,
JSON_RECEIVED_RESPONSE : self.receivedData};
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(didReceiveJSONResponse:) withObject:responseDict waitUntilDone:NO]; // ok to uses performSelector as this data is not for use on the main thread ???
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// return error to the delegate
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(didFailToReceiveDataWithError:) withObject:error waitUntilDone:NO];
}
#user1028028:
Use the following approach.
1) Maintain the operation queue reference that you are using to add DownloadOperation.
2) In connectionDidFinishLoading method, create ParseOperation instance, set the json data and add it to operation queue. Maintain ParseOperation strong reference variable in DownloadOperation and handling of cancelling of parsing operation through DownloadOperation interface.
3) After completed parsing call the UI functionality in main thread.
I hope this helps.
As lucianomarisi notes, it would usually be best to just have the first operation generate the second operation. This is usually simpler to manage. Operation dependencies aren't really that common in my experience.
That said, it's of course possible to pass data between operations. For instance, you could create a datasource property on the second operation. That would be the object to ask for its data; that object would be the first operation. This approach may require locking, though.
You can also create a nextOp property on the first operation. When it completes, it would call setData: on the second operation before exiting. You probably wouldn't need locking for this, but you might. In most cases it would be better for the first operation to just schedule the nextOp at this point (which again looks like lucianomarisi's answer).
The point is that an operation is just an object. It can have any methods and properties you want on it. And you can pass one operation to another.
Keep in mind that since an operation runs in the background, there's no reason you need to use the asynchronous interface to NSURLConnection. The synchronous API (sendSynchronousRequest:returningResponse:error: is fine for this, and much simpler to code. You could even use a trivial NSBlockOperation. Alternately, you can use the asynchronous NSURLConnection interface, but then you really don't need an NSOperation.
I also notice:
_receivedData = [NSMutableData dataWithCapacity:256];
Is it really such a small piece of JSON data? It's hard to believe that this complexity is worth it to move such a small parsing operation to the background.
(As a side note, unless you know precisely the size of the memory, there's not usually much benefit to specifying a capacity manually. Even then it's not always clear that it's a benefit. I believe NSURLConnection is using dispatch data under the covers now, so you're actually requesting a memory block that will never be used. Of course Cocoa also won't allocate it because it optimizes that out... the point is that you might as well just use [NSMutableData data]. Cocoa is quite smart about these kinds of things; you generally can only get in the way of its optimizations.)
As Rob said, unless you have any particular reason to use operations use the synchronized call. Then perform the selector on MainThread or on any other thread you need. Unless you want to separate the retrieval and parsing in separate operations or thread (explicitly).
Here is the code I was using for json retrieval and parsing:
-(BOOL) loadWithURL:(NSString*) url params: (NSDictionary*) params andOutElements:(NSDictionary*) jElements
{
NSError *reqError = nil;
NSString* urlStr = #"";//#"http://";
urlStr = [urlStr stringByAppendingString:url];
NSURL* nsURL = [NSURL URLWithString:urlStr];
//Private API to bypass certificate ERROR Use only for DEBUG
//[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:[nsURL host]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: nsURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[request setHTTPMethod:#"POST"];
NSString *postString = #"";
if(params!=nil) {
NSEnumerator* enumerator = params.keyEnumerator;
NSString* aKey = nil;
while ( (aKey = [enumerator nextObject]) != nil) {
NSString* value = [params objectForKey:aKey];
//Use our own encoded implementation instead of above Apple one due to failing to encode '&'
NSString* escapedUrlString =[value stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
//Required to Fix Apple bug with not encoding the '&' to %26
escapedUrlString = [escapedUrlString stringByReplacingOccurrencesOfString: #"&" withString:#"%26"];
//this is custom append method. Please implement it for you -> the result should be 'key=value' or '&keyNotFirst=value'
postString = [self appendCGIPairs:postString key:aKey value:escapedUrlString isFirst:false];
}
}
//************** Use custom enconding instead !!!! Error !!!!! **************
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&reqError];
if(reqError!=nil) {
NSLog(#"SP Error %#", reqError);
return NO;
}
NSString *json_string = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
//Handles Server Errors during execution of the web service that handles the call.
if([json_string hasPrefix:#"ERROR"] == YES){
NSLog(#"SP Error %#", lastError);
return NO;
}
//Very Careful!!!!!! Will stop for any reason.!!!!!!
//Handles errors from IIS Server that serves teh request.
NSRange range = [json_string rangeOfString:#"Runtime Error"];
if(range.location != NSNotFound) {
NSLog(#"SP Error %#", lastError);
return NO;
}
//Do the parsing
jElements = [[parser objectWithString:json_string error:nil] copy];
if([parser error] == nil) {
NSLog(#"Parsing completed");
} else {
jElements = nil;
NSLog(#"Json Parser error: %#", parser.error);
NSLog(#"Json string: %#", json_string);
return NO;
}
//Parsed JSON will be on jElements
return YES;
}

objective C process other events within while loop

Is there a way to force your application to process other events? I have a loop that is supposed to wait 10 seconds for a json response. The problem seems to be that the loop processes before the didReceiveData event can run.
JsonArray is an NSArray property.
Here is my code in Session.m:
-(BOOL)logIn
{
JsonArray = nil;
if (([Password class] == [NSNull class]) || ([Password length] == 0))
return NO;
if (([Username class] == [NSNull class]) || ([Username length] == 0))
return NO;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL_ALL_PROPERTIES]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
if (!connection)
{
return NO;
}
int time = CFAbsoluteTimeGetCurrent();
while (!JsonArray)
{
if (CFAbsoluteTimeGetCurrent() - time == 10)
{
NSLog(#"breaking loop");
break;
}
// process events here
}
if (!JsonArray) return NO;
NSLog(#"JsonArray not nil");
return YES;
}
didReceiveData handler:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"Data received");
NSError *e = nil;
JsonArray = nil;
JsonArray = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&e];
if (!JsonArray)
{
JsonArray = nil;
Connected = NO;
return;
}
Connected = YES;
}
Embrace the asynchronous nature of what you're trying to do. Remove the return from the login method and add a completion block instead. Store the completion block in a property (copy) and call it from connectionDidFinishLoading:.
typedef void (^MYCompletionHandler)(BOOL success);
#property (nonatomic, copy) void (^MYCompletionHandler)(bool *) completion;
- (void)loginUserWithCompletion:(MYCompletionHandler)completion {
self.completion = completion;
// start your login processing here
}
// when the login response is received
self. completion(##DID_IT_WORK##);
Your current code doesn't work because the main runloop isn't running while your hard loop is running, so the delegate methods are queued waiting to be processed. Don't try to work around it with the run loop, deal with the asynchronous nature properly.
The didReceiveData method will be called when the data has been received. If you need to process the data returned, do it in the delegate method.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// process your data in here
}
The logIn function has to finish before didReceiveData will be called. You cannot block for a response. That's the nature of NSURLConnection.

Spinning wheel blocks lazy image load

It sounds a bit strange, but since i use a spinning wheel indicator, the lazy image load don't works for the first image views (these once that are shown in the first screen). If the user scrolls down all other Images in the TableView loading correctly by a lazy download.
The main problem is, that NSURLConnection didn't calls didReceiveData.
- (void)startDownload
{
self.activeDownload = [NSMutableData data];
BOOL firstCell = (self.indexPathInTableView.row==0 && self.indexPathInTableView.section==0);
if(firstCell){
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:
[NSURLRequest requestWithURL:
[NSURL URLWithString:newsContent.title_picture]] delegate:self];
NSLog(#"Get Title Pic %# (%#)",newsContent.title, newsContent.title_picture);
self.imageConnection = conn;
}else{
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:
[NSURLRequest requestWithURL:
[NSURL URLWithString:newsContent.cover_picture]] delegate:self];
NSLog(#"Get Thumb Pic %# (%#)",newsContent.title, newsContent.cover_picture);
self.imageConnection = conn;
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"[NewsPicture][connection]didReceiveData");
[self.activeDownload appendData:data];
}
Edit: Added didReceiveResponse
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"[NewsPicture][connection]didReceiveResponse");
}
I'll get the Log "Get Thumb Pic ... (...)" with a correct Url, but for the for the first 5 rows (they fills the screen of an iPhone 4) i don't get the Log "[NewsPicture][connection]didReceiveData".
This is the way how i call the Indicator:
// Spinning Wheel
HUD = [[MBProgressHUD alloc] initWithView:self.view];
HUD.tag = 1000;
[self.view addSubview:HUD];
HUD.delegate = self;
HUD.labelText = #"wird geladen";
HUD.minShowTime = 25;
HUD.dimBackground = YES;
[HUD show:true];
[HUD showWhileExecuting:#selector(doWhileLoadingNews) onTarget:self withObject:nil animated:NO];
and if i only call
[self doWhileLoadingNews];
at this place all works fine, but without in indicator for loading data.
How could i fix it? (I can post more Code oder Informations if you need)
Edit: I still couldn't fix it. Is it possible to catch the result in another way then calling the 'didReceiveData'?
Edit: Added didReceiveResponse but with the same result, didReceiveResponse is also not called.

Objective-c threading and cpu performance

I have an app which downloads some files from the server in few threads. The problems is that it is giving a heavy load to the CPU (hitting to 80%). What can be done to make it better? I made similar app on Windows with C#, and the cpu usage never goes above 5%.
EDIT: This code has been changed after getting some suggestions below. The problem now is, that the download never reaches 100% when I set [queue setMaxConcurrentOperationCount:6]. If I change the asynchronous NSURLConnection back to sendSynchronous call it works, when I change the above OperationCount to 1, also works.
This is how I add NSOperations to the queue (may be large, like 800).
int chunkId = 0;
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
chunkId = 0;
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
DownloadChunkOperation *operation = [[DownloadChunkOperation alloc] initWithDownloadObject:download
downloadFile:downloadFile downloadChunk:downloadChunk andChunkId:chunkId];
[queue addOperation:operation];
chunkId++;
}
}
#import "DownloadChunkOperation.h"
#import "Download.h"
#import "DownloadFile.h"
#import "DownloadChunk.h"
#interface DownloadChunkOperation()
#property(assign) BOOL isExecuting;
#property(assign) BOOL isFinished;
#end
#implementation DownloadChunkOperation
#synthesize download = _download;
#synthesize downloadFile = _downloadFile;
#synthesize downloadChunk = _downloadChunk;
#synthesize isFinished = _isFinished;
#synthesize isExecuting = _isExecuting;
- (id) initWithDownloadObject:(Download *)download downloadFile:(DownloadFile *)downloadFile downloadChunk:(DownloadChunk *)downloadChunk andChunkId:(uint32_t)chunkId
{
self = [super init];
if (self) {
self.download = download;
self.downloadFile = downloadFile;
self.downloadChunk = downloadChunk;
self.chunkId = chunkId;
}
return self;
}
- (void) start
{
if ([self isCancelled]) {
[self setIsFinished:YES];
[self setIsExecuting:NO];
return;
}
[self setIsExecuting:YES];
[self setIsFinished:NO];
[self.downloadChunk setChunkState:cDownloading];
downloadPath = [[NSString stringWithFormat:#"%#/%#", [self.download downloadFolder], [self.download escapedTitle]] stringByExpandingTildeInPath];
NSURL *fileURL = [[NSURL alloc] initWithString:[self.downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", [self.downloadChunk startingByte], [self.downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
// IMPORTANT! The next line is what keeps the NSOperation alive for the during of the NSURLConnection!
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
if (connection) {
NSLog(#"connection established!");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!self.isFinished);
} else {
NSLog(#"couldn't establish connection for: %#", fileURL);
}
}
- (BOOL) isConcurrent
{
return YES;
}
- (void) connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response
{
receivedData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Not cancelled, receive data.
if (![self isCancelled]) {
[receivedData appendData:data];
self.download.downloadedBytes += [data length];
return;
}
// Cancelled, tear down connection.
[self setIsExecuting:NO];
[self setIsFinished:YES];
[self.downloadChunk setChunkState:cConnecting];
[self->connection cancel];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self setIsExecuting:NO];
[self setIsFinished:YES];
NSLog(#"Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *chunkPath = [downloadPath stringByAppendingFormat:#"/%#.%i", [self.downloadFile fileName], self.chunkId];
NSError *saveError = nil;
[receivedData writeToFile:chunkPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil) {
NSLog(#"Download save failed! Error: %#", [saveError description]);
}
else {
NSLog(#"file has been saved!: %#", chunkPath);
}
[self setIsExecuting:NO];
[self setIsFinished:YES];
[self.downloadChunk setChunkState:cFinished];
if ([self.download downloadedBytes] == [self.download size])
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:self.download];
}
#end
You should not create threads yourself. Use dedicated API like NSOperationQueue or even GCD directly for this purpose. They know better about hardware limits, virtual cores, etc. and support priority settings.
You shouldn't use +sendSynchronousRequest: either. Wrapping your -downloadChunk method in a dispatch call as suggested by charith won't help you improve performance, as +sendSynchronousRequest: blocks the thread until new data comes in and forces GCD to spawn new threads.
Use the asynchronous API of NSURLConnection using delegate callbacks. You can also wrap your NSURLConnection code inside a NSOperation subclass and use NSOperationQueue to manage the downloads: Using NSURLConnections
If you don't want to write the NSOperation subclass yourself, you can also use a 3rd party framework like AFNetworking.
Try with GCD blocks and global queues. This is the apple recommended way now for concurrency ex:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
[self downloadChunk:objDownload];
});

NSURLConnection is not calling didFailWithError.

I am attempting to write a bit of code that checks the URL of a datasource, then populates an array with objects from that URL. It actually works well, but if there is a problem with the web connection or the address I want to populate the array with data from a bundled file. The issue I am having is that the connection didFailWithError method is never called. I tried passing a simple string but it does not call. I want the app to still function for people who are using ipod touch or are in airplane mode.
connection didReceiveResponse is working without issue.
This is what I'm working with.
- (void)loadListData{
NSLog(#"Loading data from sources");
NSURLRequest *listURLRequest = [NSURLRequest requestWithURL:integerPhoneListURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:1.0];
[[NSURLConnection alloc] initWithRequest:listURLRequest delegate:self];
if (!listConnectFail){
phoneListJSON =[NSData dataWithContentsOfURL:integerPhoneListURL];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:phoneListJSON waitUntilDone:YES];
} else {
//This will tell us if there is an error loading the file
NSLog(#"File not found on web init from file");
phoneListJSON =[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"contactlist" ofType:#"json"]];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:phoneListJSON waitUntilDone:YES];
}
//Initialize the filtered list with array of customer objects. Based on original data
filteredList = [[NSMutableArray alloc] init];
for (NSDictionary *dict in phoneListOriginal) {
contact *single = [[contact alloc] init];
single.fName = [dict objectForKey:#"fName"];
single.lName = [dict objectForKey:#"lName"];
single.extension = [dict objectForKey:#"extension"];
single.title = [dict objectForKey:#"title"];
single.department = [dict objectForKey:#"department"];
single.cellNumber = [dict objectForKey:#"cellNumber"];
//NSLog(#"%#", single.lName);
[filteredList addObject:single];
}
NSLog(#"Array filteredLIst contains %d records",[filteredList count]); }
-(void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
listConnectFail = YES;
NSLog(#"Connection Failed, pulling from file"); }
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
listConnectFail = NO;
NSLog(#"Connection Succeeded, populating from API");
}
I know it is probably something stupid that I am not seeing, but I could use the help to see what I don't
Thanks in advance!
How did you confirm that your delegate did not receive the message? Did you check the log?
Your code seems to assume that 'listConnectFail' will be set immediately after the NSURLConnection's init is done, which is not necessarily the case.
[[NSURLConnection alloc] initWithRequest:listURLRequest delegate:self];
if (!listConnectFail){...}
The NSURLConnection documentation states that 'The delegate will receive delegate messages as the load progresses.'
However, I am not sure about the airplane mode, maybe this particular error can be detected synchronously.