I know that issue has been reported tons of times but almost all the answers concerned non ARC projects and I have been stuck for days now. I have looked in many forums and articles. I hope I could find some help here.
I use an asynchronous NSURLConnection request in order to download pictures, with IOS 5, using ARC, as follow (code inspired from the book "Learning IPAd programming") :
-(void) dowloadImageAtURL:(NSURL *)URL
{
if (URL)
{
self.image = nil;
self.receivedData = [[NSMutableData alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:URL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
[[NSURLCache sharedURLCache] setMemoryCapacity:0];
[[NSURLCache sharedURLCache] setDiskCapacity:0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request // Building asynchronous connection
delegate:self
startImmediately:NO]; // This is the key to not run the connection synchronous
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // We use the current loop but it will still be asynchronous
// because of the mode added to the current loop
[connection start];
request = nil;
}
}
#pragma marks - NSURLConnection delegate Methods
/*
Called when the web server responds to the request.
When the method is called, the receivedData property is reset with a length of zero, clearing any previously stored data.
This ensures that only the data received from the final request is captured (see NSURLConnectionDelegate reference).
*/
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[self.receivedData setLength:0];
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
sharedCache = nil;
connection = nil;
}
/*
Called when data is received from the network. The method can be called a multiple times during a single request.
The data is appended to the data already stored in the receivedData property.
*/
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.receivedData appendData:data];
}
/*
Called after the request has completed all data and all data has been received. This method converts
the data stored to a UIImage object.
*/
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
self.image = [UIImage imageWithData:self.receivedData];
self.receivedData = nil;
connection = nil;
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
sharedCache = nil;
// Here we post a notification to the observer
[[NSNotificationCenter defaultCenter] postNotificationName:#"com.perfectmemory.famille.imageupdated" object:self];
}
/*
Called if an error is detected at any time during the download process.
*/
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
self.receivedData = nil;
connection = nil;
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
sharedCache = nil;
// Here we post a notification to the observer
[[NSNotificationCenter defaultCenter] postNotificationName:#"com.perfectmemory.famille.imageupdated" object:self];
}
When running this code, the download works well but I can see leaks in the XCode instruments
URLConnectionLoader::LoaderConnectionEventQueue
URLConnection::scheduleWithRunLoop
CFURLResponse
URLConnectionInstanceData
I have used the following helps to try to fix the issue, such as clearing the cache, but no success :
NSURLConnection Leaks -- Why?
http://www.friendlydeveloper.com/2010/04/successfully-working-around-the-infamous-nsurlconnection-leak/
NSURLConnection leak?
I also tried to release the connection in each concerned delegate methods with :
connection = nil
But still no success. I really don't understand what is going on. I can't find unreleased objects. Could you please help me ? It's very frustrating since I guess this asynchronous download request is very common.
Thanks a lot in advance.
Related
I have looked at NSURLConnectionDelegate connection:didReceiveData not working already, but there didn't seem to be any good result from that, so I am curious why I am not able to get any data.
I put in breakpoints in didReceiveResponse and didReceiveData.
It does print out "connection succeeded", so I know that the connection is started.
I am using ARC for memory management.
- (void)load {
request = [NSMutableURLRequest requestWithURL:myURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:60];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (conn) {
[conn start];
NSLog(#"connection succeeded, %s", [myURL description]);
responseData = [NSMutableData data];
} else {
NSLog(#"connection failed");
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
responseData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[responseData appendData:data];
}
UPDATE:
To see how I test this look at Asynchronous unit test not being called by SenTestCase.
I did implement the two methods mentioned by jonkroll, in his answer, I just didn't show them, but, they also aren't being called.
I had added [conn start] only because it wasn't working, and I was hoping that may solve it, but no such luck.
When you declare your connection like this:
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
You are creating a local pointer. When your method completes, since it was the last strong reference to the NSURLConnection, ARC releases it. You need to use a strong ivar (and/or) property to hold a strong reference to the NSURLConnection you create.
Edit
Here is basic sample of code that I tested in a sample project. Give it a run. Verbose logging helps.
#implementation <#Your class here#> {
// With ARC ivars are strong by default
NSMutableData *_downloadedData;
NSURLConnection *_connection;
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSHTTPURLResponse *realResponse = (NSHTTPURLResponse *)response;
if (realResponse.statusCode == 200){
// Really any 2** but for example
_downloadedData = [[NSMutableData alloc] init];
NSLog(#"Good response");
} else {
NSLog(#"Bad response = %i",realResponse.statusCode);
}
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
if (connection == _connection){
[_downloadedData appendData:data];
NSLog(#"Getting data...");
}
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
if (connection == _connection){
_connection = nil;
NSLog(#"We're done, inform the UI or the delegates");
}
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
_connection = nil;
NSLog(#"Oh no! Error:%#",error.localizedDescription);
}
- (void)load {
NSURL *url = [NSURL URLWithString:#"http://www.google.com/"];
NSURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:60];
// Assign strong pointer to new connection
_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
NSLog(#"Connection was initialized? = %#",(!!_connection)?#"YES":#"NO");
}
#end
The NSURLConnection method initWithRequest starts an asynchronous request for data from a url. Because the request is done asynchronously you can't expect to work with the response in the same method in which the request is invoked. Instead you need to do so in the NSURLConnection's delegate callback methods. You have already implemented didReceiveResponse: and didReceiveData:, but there are a couple others that will be useful to you.
If you want to look at the contents of the response you should do so in connectionDidFinishLoading:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// response is complete, do something with the data
NSLog(#"%#", responseData);
}
The fact that your code prints out "connection succeeded" doesn't really mean that the request was successful, only that the NSURLConnection object was created successfully. To test whether there was a problem with the connection you can implement the delegate method connection:didFailWithError:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(#"Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
Also there is no need to call [conn start]. The request will be started automatically when you call initWithRequest:
I suggest reading Apple's documentation on Using NSURLConnection for more details.
Can any one of you post the code snippet, example tutorial on handling the multiple
NSURLConnections from the same viewController using cocoa Touch framework....
Thanks for all your future help.....
I handled multiple NSUrlConnections using a NSMutableDictionary which keeps track of which instance of NSMutableData a particular NSURLConnection should save its result to.
At the beginning of my class I define:
NSMutableDictionary *dataDictionary;
Then in my loadData method, I have:
// Create the request
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:currentCam]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:30];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
NSMutableData *receivedData = receivedData = [[NSMutableData alloc] init];
//keep track of this connection by adding it to subViewDictionary and dataDictionary with appropriate objects.
[dataDictionary setObject:receivedData forKey:[theConnection description]];
}
else {
NSLog(#"ERROR DOWNLOADING WITH NSURLCONNECTION");
}
[theConnection release];
I use [theConnection description] as my key and an instance of MSMutableData as the object in my dictionary, so later I can look up which instance goes with a particular connection. If you fail to do this you can have issues with data corruption (multiple connections can all save their data to the same variable).
Then, I define the following NSURlConnection delegate methods:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSMutableData *)data
{
//look up in dictionary to find out which recievedData instance to use.
NSMutableData *theReceivedData = [dataDictionary objectForKey:[connection description]];
// Append the new data to receivedData.
[theReceivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
//Lookup in the dictionary to find the correct instance of recievedData to put image into.
NSMutableData *theReceivedData = [dataDictionary objectForKey:[connection description]];
[theReceivedData setLength:0];
[activityIndicator stopAnimating];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// inform the user that there was an error
NSLog(#"Connection failed! Error - localizedDescription:%# NSURLErrorFailingURLStringErrorKey:%#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
NSMutableData *theReceivedData = [dataDictionary objectForKey:[connection description]];
[theReceivedData release];
//remove keys for this connection since it did not load.
[dataDictionary removeObjectForKey:[connection description]];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//look up correct instance of recievedData in teh dictionary for this connection
NSMutableData *theReceivedData = [dataDictionary objectForKey:[connection description]];
NSLog(#"Succeeded! Received %d bytes of data",[theReceivedData length]);
//---do stuff with data here//
[theReceivedData release];
}
There is a good tutorial on NSURlConnection here for a single connection. My code is based on that with the addition of the NSMutableDictionary to keep track of each NSUrlConnection and each NSMutableData instance.
I hope that makes sense and is helpful!
See this link for the answer. It wraps nsurlconnection with asihttp, which makes your life much easier.
Does ASIHTTP support multi threads?
I saw there is a method for synchronous, like if I wanted to do something like:
-(IBAction)doNSURLConnSync {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSError *error = nil;
NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
}
How does it perform differently than if I did asynchronous:
-(IBAction)doNSURLConnASync {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self];
if (connection) {
responseData = [[NSMutableData alloc] init];
[webview loadHTMLString:#"" baseURL:nil];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
else {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error"
message:#"Network error occured"
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"%s", __FUNCTION__);
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[responseData appendData:data];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[connection release]; // got passed in as a param. we are done with it now.
[webview loadData:responseData
MIMEType:nil
textEncodingName:nil
baseURL:nil];
[responseData release];
}
Also, with my doNSURLConnSync method, I am just trying to load a UIWebView. Is there a reason why it doesn't? The button just sits there and stays highlighted, while it tries to access the webpage, but does nothing in the end, compared to the asynchronous version.
Also, for networkactivityindicator in my asynchronous code, I wanted to set my UIWebView to blank, have the indicator on while my webview loads, and then turn off the network activity indicator once the page loads. However, if I delete the loadHTMLString method, the network activity indicator works as it's supposed to, but with the loadHTMLString, the UIWebView goes blank, but the network activity indicator does not. Any thoughts? Thanks.
First, for the syncrhonous:
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
This is the method signature, when you call the synchronous request, it wil return the data for you to display on the UIWebView and you have to call the UIWebView to display the data. However, the synchronous calling will block your UI until all the data come back. So, be careful with UX.
NSUrlConnection sendSynchronousRequest
For the asynchronous, it will not block your UI, user can still do everything they want with it, like go back to the previous screen. So, usually, it is recommended for big and long network
I don't know why it doesn't show your indicator. But why do you need this line : [webview loadHTMLString:#"" baseURL:nil]; . You only need to call it after you got your HTML response
A synchronous request ties up the main thread, which you should reserve for UI widget updates.
Doing an asynchronous request on a background thread frees up the main thread to update the UI.
Pull your UI update code (indicator view and web view) into separate methods, calling them on the main thread with -performSelectorOnMainThread:withObject:waitUntilDone:
[self performSelectorOnMainThread:#selector(updateWebview) withObject:nil waitUntilDone:YES];
I was previously downloading images for my app by using dataWithContentsOfURL to download a jpg then writeToFile to save it.
I recent;y started using an NSURLConnetion to do the same, but now I am getting the follwoing errors and a crash:
Corrupt JPEG data: 87 extraneous bytes
JPEG datastream contains no image
I know these images are not corrumpt, as the app was downloading them fine using the previous method. Here is my code:
-(void) downloadSave {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
NSString *tempString = [[NSString alloc]initWithFormat:#"http://www.mysite.com/%#.jpg",chartFileName];
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:tempString]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData to hold the received data.
// receivedData is an instance variable declared elsewhere.
mutableData = [[NSMutableData data] retain];
self.image = nil;
NSLog(#"connection exists");
[NSURLConnection connectionWithRequest:theRequest delegate:self];
} else {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Connection Error" message:#"There was an error contacting the chart servers. Please try again." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
[alert release];
[activityIndicator stopAnimating];
}
// NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
// NSUserDomainMask, YES);
// NSString *docsPath = [paths objectAtIndex:0];
// NSString *downloadPath = [[[NSString alloc]initWithFormat:#"http://www.mysite.com/%#.jpg",chartFileName]autorelease];
// downloadedChartData = nil;
[pool drain];
[pool release];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// This method is called when the server has determined that it
// has enough information to create the NSURLResponse.
// It can be called multiple times, for example in the case of a
// redirect, so each time we reset the data.
// receivedData is an instance variable declared elsewhere.
NSLog(#"got to connection did receive response");
[mutableData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Append the new data to receivedData.
// receivedData is an instance variable declared elsewhere.
[mutableData appendData:data];
// NSLog(#"got some data, total: %i",mutableData.length);
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
// release the connection, and the data object
// [connection release];
// receivedData is declared as a method instance elsewhere
// self.mutableData = nil;
// inform the user
//NSLog(#"Connection failed! Error - %# %#",
// [error localizedDescription],
// [[error userInfo] objectForKey:NSErrorFailingURLStringKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// do something with the data
// receivedData is declared as a method instance elsewhere
NSLog(#"Succeeded! Received %d bytes of data",[mutableData length]);
[connection release];
// release the connection, and the data object
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *docsPath = [paths objectAtIndex:0];
self.image = nil;
NSString *savePath = [[[NSString alloc]initWithFormat:#"%#/%#.jpg",docsPath, chartFileName]autorelease];
[mutableData writeToFile:savePath atomically:YES];
self.mutableData = nil;
You are initializing and starting two NSURLConnections with the same delegate. As your delegate methods do not check which connection called them you are mixing up the bytes of two times your image in one NSMutableData instance.
// Creates, initializes and starts an instance of NSURLConnection
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
...
// Creates, initializes and starts another instance of NSURLConnection, with same request and delegate
[NSURLConnection connectionWithRequest:theRequest delegate:self];
Both connections message the same implementations on the same delegate instance, which means their data is written into the same NSMutableData in random order.
I would suggest to simply get rid of the line:
[NSURLConnection connectionWithRequest:theRequest delegate:self];
Another thing: why are you using an autorelease pool in downloadSave? If you call it from the main Thread you have only one NSURLRequest autoreleased in that pool. If you call it from an other Thread you have to take care, that the runloop of that thread is setup and running, or you wouldn't receive any delegate callbacks at all.
Have a look to this code snippet:-
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[webData setLength: 0];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"Recieving Data...");
[webData appendData:data];
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(#"ERROR with theConenction");
[connection release];
[webData release];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"DONE. Received Bytes: %d", [webData length]);
NSLog(theXML);
}
I am calling a SOAP web service.There are no errors or warnings displayed in my code.
When I hit the web service through safari it works fine. But the problem arises when I try
hit it through my codes.
Everything works fine but the connection:didRecieveData does not gets called.
Thus, I get no data in the webData variable. This webData is a NSMutableData object.
The problem seems to be silly but any one with any answers ....
Thank You All.
I suspect you are having a memory management issue. I could be mistaken on this, but I believe that even:
NSURLConnection* connection=[[NSURLConnection alloc] initWithRequest:request delegate:self];
won't work, because connection will be released at the end of the containing method, when connection goes out of scope. Make sure NSURLConnection *connection and NSMutableData *data are declared as member variables where ever you are doing this, and that you alloc and init them appropriately. My code usually looks like:
NSURLRequest *request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:30.0];
// cancel any old connection
if(connection) {
[connection cancel];
[connection release];
}
// create new connection and begin loading data
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(connection) {
// if the connection was created correctly, release old data (if any), and alloc new
[data release];
data = [[NSMutableData data] retain];
}
Also, release the connection and data in dealloc. For good measure, release and set them to nil at the very end of didFailWithError and didFinishLoading:
[connection release];
connection = nil;
[data release];
data = nil;
Good luck; I've done this a million times, let me know if you cannot get it working.
You don't happen to be calling the NSConnection in a thread do you? If you are then what's happening is that the thread is terminating before NSConnection and its delegates have finished so it'll just bomb out without an error.
A workaround for this is in this thread
You're not getting any error messages in didFailWithError either? Kind of a silly suggestion, but are you sure you're setting the proper NSURLConnection delegate?
NSURLConnection* connection=[[NSURLConnection alloc] initWithRequest:request delegate:self];
Sometimes it's something small like that.
Another idea is to drop in a toolkit like ASIHTTPRequest and see if it works going through them.
There also could be problems, if are trying to start NSURLConnection from another Thread.
Please call method [connection start] on main thread, if you have not customized Run Loop for it.