I'm building an iPhone app that aggregates data from several different data sources and displays them together in a single table. For each data source, I've created a class (WeatherProvider, TwitterProvider) that handles connecting to the datasource, download the data and storing it in an object.
I have another class ConnectionManager, that instantiates and calls each of the two provider classes, and merges the results into a single NSArray which is displayed in the table.
When I run the program calling just the WeatherProvider or just the TwitterProvider, it works perfectly. I can call each of those objects again and again to keep them up-to-date without a problem. I can also call TwitterProvider and then WeatherProvider without a problem.
However, if I call WeatherProvider then TwitterProvider, the TwitterProvider hangs just as I call: [[NSURLConnection alloc] initWithRequest:request delegate:self];.
Other points:
- it doesn't seem to matter how long between when I call WeatherProvider and TwitterProvider, TwitterProvider still hangs.
- in WeatherProvider, I'm using NSXMLParser with NSAutoreleasePool to parse the output from the WeatherProvider.
- ConnectionManager creates an instance of WeatherProvider and TwitterProvider when the application is started, and reuses those instances when the user requests a data refresh.
- I ran the app with the activity monitor connected and it verifies that the app is basically just hanging. No CPU usage, or additional memory allocations or network activity seems to be happening.
There's a bunch of code spread across a couple files, so I've tried to include the relevant bits (as far as I can tell!) here. I very much appreciate any help you can provide, even if it is just additional approaches to debugging.
WeatherProvider
-(void)getCurrentWeather: (NSString*)lat lon:(NSString*)lon lastUpdate:(double)lastUpdate
{
double now = [[NSDate date] timeIntervalSince1970];
NSString *noaaApiUrl;
// don't update if current forecast is < than 1 hour old
if(now - lastUpdate < 3600)
{
[[self delegate] weatherUpdaterComplete:1];
return;
}
// if we already have a forecast, delete and refill it.
if(forecast)
{
[forecast release];
forecast = [[WeatherForecast alloc] init];
}
forecast.clickThroughUrl = [NSString stringWithFormat:#"http://forecast.weather.gov/MapClick.php?lat=%#&lon=%#",
lat, lon];
noaaApiUrl = [NSString stringWithFormat:#"http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?lat=%#&lon=%#&format=24+hourly",
lat, lon];
NSURLRequest *noaaUrlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:noaaApiUrl]];
[[NSURLConnection alloc] initWithRequest:noaaUrlRequest delegate:self];
}
#pragma mark -
#pragma mark NSURLConnection delegate methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.noaaData = [NSMutableData data];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[noaaData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
self.noaaConnection = nil;
[[self delegate] weatherUpdaterError:error];
[connection release];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// Spawn a thread to fetch the data so UI isn't blocked while we parse
// the data. IMPORTANT - don't access UIKit objects on 2ndary threads
[NSThread detachNewThreadSelector:#selector(parseNoaaData:) toTarget:self withObject:noaaData];
// the noaaData will be retailed by the thread until parseNoaaData: has finished executing
// so, we'll no longer need a reference to it in the main thread.
self.noaaData = nil;
[connection release];
}
#pragma mark -
#pragma mark NSXMLParser delegate methods
- (void)parseNoaaData:(NSData *)data
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser parse];
[parser release];
[pool release];
}
TwitterProvider
-(void)getLocationTimeline:(NSString*)lat lon:(NSString*)lon lastUpdate:(double)lastUpdate refreshUrl:(NSString*)newUrl
{
NSString *updateURL;
if(tweets.count > 1)
[tweets removeAllObjects];
updateURL = [NSString stringWithFormat:#"http://search.twitter.com/search.json?geocode=%#,%#,1mi&rpp=%#&page=1", lat, lon, TWITTER_LOCAL_UPDATES_URL_RESULTS];
_serviceResponse = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:updateURL]];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
#pragma mark -
#pragma mark NSURLConnection delegate methods
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *responseString = [[NSString alloc] initWithData:_serviceResponse encoding:NSUTF8StringEncoding];
// parse tweets
[responseString release];
[_serviceResponse release];
[connection release];
// tell our delegate that we're done!
[[self delegate] twitterUpdaterComplete:tweets.count];
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
[_serviceResponse setLength:0];
}
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
[_serviceResponse appendData:data];
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[connection release];
[[self delegate] twitterUpdaterError:error];
}
I was able to fix this by removing the threading in WeatherUpdater that was being used for the NSXMLParser. I had 'barrowed' that code from Apple's SiesmicXML sample app.
Related
Hai here Am downloading a video and storing it to documents folder in iphone... but when i click on download button video will start downloading and progress will show nicely... but when i go back to previous view controller and came back to the downloading controller during downloading the progress view status will not show the current status... can anybody help me to solve this? thanks in advance...
Here is my code...
- (void)downLoad:(UIButton *)sender {
_downloadBtn.hidden = YES;
_videoData = [[NSMutableData alloc] init];
_resourceName = [_resourceNameArray objectAtIndex:sender.tag];
_resourceType = [_contentTypesArray objectAtIndex:sender.tag];
NSString *urlStr = [NSString stringWithFormat:#"%#",[_videoUrlArray objectAtIndex:sender.tag]];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]] delegate:self ];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"Downloading Started");
_length = [response expectedContentLength];
NSLog(#"Size:%0.2f",_length);
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_videoData appendData:data];
float progress = (float)[_videoData length]/(float)_length;
NSLog(#"Progress:%0.2f",progress);
//_timeLabel.text = [NSString stringWithFormat:#"%0.2F%%",progress*100];
[_progressView setProgress:progress animated:YES];
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"didFailWithError");
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self saveLocally:_videoData res:_resourceName type:_resourceType];
NSLog(#"File Saved !");
}
Download screen -> previous screen -> Download screen. Let's try:
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_videoData appendData:data];
float progress = (float)[_videoData length]/(float)_length;
NSLog(#"Progress:%0.2f",progress);
NSNumer *n = [NSNumber numberWithFloatValue:progress];
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_DOWNLOAD_PROGRESS object:n];
//_timeLabel.text = [NSString stringWithFormat:#"%0.2F%%",progress*100];
[_progressView setProgress:progress animated:YES];
}
When you go back to download screen, you should update progress by receive notification
#define NOTIFICATION_DOWNLOAD_PROGRESS #"NOTIFICATION_DOWNLOAD_PROGRESS"
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(processProgressBar:) name:NOTIFICATION_DOWNLOAD_PROGRESS object:nil];
}
- (void) processProgressBar:(NSNumber *) progress
{
[_progressView setProgress: progress.floatValue];
}
I have a view in which I am fetchind data from the web - calling a php script and get a JSON response. The way that I do it is by performing an NSUrlRequest.
The way I do it is like this:
-(void) viewDidLoad
{
.....
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:url]];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"didReceiveResponse");
[self.responseData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"didFailWithError");
NSLog(#"Connection failed: %#", [error description]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"connectionDidFinishLoading");
NSLog(#"Succeeded! Received %d bytes of data",[self.responseData length]);
...
}
Now I have another UIView, that wants to be displayed when that screen opens - like a splash screen only for this screen (which is not the starting screen) - it would be a how to use it screen. I want to be visible only for 5 secs for example.
If I place the [self openCustomView] inside the ViewDidLoad then I get a warning:
is not in the root view hierarchy
and the view does not open.
If I put in the connectionDidFinishLoading then I am waiting for the data to download which can take time.
Is there a way to preview that splash screen and download the data in the background?
My openCustomView is this:
UIStoryboard *sb = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
CustomView *vc = [sb instantiateViewControllerWithIdentifier:#"CustomViewID"];
vc.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:vc animated:YES completion:NULL];
This is a bit of guessing but, from the message, I suspect you're calling openCustomView from the wrong spot. I don't believe self (and its views) are considered part of the hierarchy during loading. I'd try viewDidAppear.
Im am trying to properly integrate MBProgressHUD in a project while i am downloading and processing some data. Forgive me my ignorance if i am asking stupid things, but i'm a complete noob...
I have a "Data" class that is handling my HTTPRequests, so i thing here is the proper place to put some stuff in:
-(void)resolve
{
// Create the request.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[[NSURL alloc] initWithString:[self url]]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSLog(#"Resolving content from: %#",[self url]);
// 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.
receivedData = [[NSMutableData data] retain];
} else {
NSLog(#"Content - resolve: connection failed");
}
// Here is the part that it makes me crazy...
HUD = [[MBProgressHUD showHUDAddedTo:self.view animated:YES] retain];
return;
}
- (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.
expectedLength = [response expectedContentLength];
currentLength = 0;
HUD.mode = MBProgressHUDModeDeterminate;
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
currentLength += [data length];
HUD.progress = currentLength / (float)expectedLength;
// Append the new data to receivedData.
// receivedData is an instance variable declared elsewhere.
[receivedData appendData:data];
return;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[HUD hide:YES];
// release the connection, and the data object
[connection release];
// receivedData is declared as a method instance elsewhere
[receivedData release];
// inform the user
NSLog(#"Content - Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
return;
}
Now i know that putting the "showHUDaddedTo" in the -(void)resolve is not the right way...
I am calling this data function from a view controller with an IBaction like this:
-(IBAction) btnClicked:(id) sender {
if ([[issue status] intValue] == 1 ) // issue is not downloaded
{
[(Content *)[issue content] resolve];
//HUD = [[MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES] retain];
}
else // issue is downloaded - needs to be archived
{
NSError * error = nil;
[[NSFileManager defaultManager] removeItemAtPath:[(Content *)[issue content] path] error:&error];
if (error) {
// implement error handling
}
else {
Content * c = (Content *)[issue content];
[c setPath:#""];
[issue setStatus:[NSNumber numberWithInt:1]];
[buttonView setTitle:#"Download" forState:UIControlStateNormal];
[gotoIssue setTitle:#"..." forState:UIControlStateNormal];
// notify all interested parties of the archived content
[[NSNotificationCenter defaultCenter] postNotificationName:#"contentArchived" object:self]; // make sure its persisted!
}
}
}
Simply said: i would like to call all the MBProgressHUD stuff from my Conten.m file in the moment i push the download button in my IssueController.m file. I think you see now where my problem is: i am not a coder ;-) Any help is appreciated.
Cheers,
sandor
I would try and use the HUD method showWhileExecuting. Wrap your calls up in a new method and call that to download your information. When your method finishes, so will your HUD.
Also, is there a reason why you are retaining your calls for the HUD? I have not seen that before and I'm not sure if you need to retain those as they are autoreleased.
I'm creating an app which communicates alot with a server. I want to make sure I have only 1 connection at a time - only 1 request pending. I want so that if I try to send another request, It will wait until the current one is finished before sending the next.
How can I implement This?
Tnx!
I don't know of any automatic mechanism to do this. So you have to write a new class yourself (let's call it ConnectionQueue):
Basically, instead of a creating the NSURLConnection directly, you call a method of your ConnectionQueue class (which should have exactly one instance) taking the NSURLRequest and the delegate as a parameter. Both are added to a queue, i.e. a separate NSArray for the requests and the delegates.
If the arrays contains just one element, then no request is outstanding and you can create a NSURLConnection with the specified request. However, instead of the delegate passed to the method, you pass your ConnectionQueue instance as the delegate. As a result, the connection queue will be informed about all actions of the connection. In most cases, you just forward the callback to the original delegate (you'll find it in the first element of the delegate array).
However, if the outstanding connection completes (connection:didFailWithError: or connectionDidFinishLoading: is called), you first call the original delegate and then remove the connection from the two arrays. Finally, if the arrays aren't empty, you start the next connection.
Update:
Here's some code. It compiles but hasn't been tested otherwise. Furthermore, the implementation of the NSURLConnectionDelegate protocol is incomplete. If you're expecting more than implemented callbacks, you'll have to add them.
Header File:
#import <Foundation/Foundation.h>
#interface ConnectionQueue : NSObject {
NSMutableArray *requestQueue;
NSMutableArray *delegateQueue;
NSURLConnection *currentConnection;
}
// Singleton instance
+ (ConnectionQueue *)sharedInstance;
// Cleanup and release queue
+ (void)releaseShared;
// Queue a new connection
- (void)queueRequest:(NSURLRequest *)request delegate:(id)delegate;
#end
Implementation:
#import "ConnectionQueue.h"
#implementation ConnectionQueue
static ConnectionQueue *sharedInstance = nil;
+ (ConnectionQueue*)sharedInstance
{
if (sharedInstance == nil)
sharedInstance = [[ConnectionQueue alloc] init];
return sharedInstance;
}
+ (void)releaseShared
{
[sharedInstance release];
sharedInstance = nil;
}
- (id)init
{
if ((self = [super init])) {
requestQueue = [NSMutableArray arrayWithCapacity:8];
delegateQueue = [NSMutableArray arrayWithCapacity:8];
}
return self;
}
- (void)dealloc
{
[requestQueue release];
[delegateQueue release];
[currentConnection cancel];
[currentConnection release];
[super dealloc];
}
- (void)startNextConnection
{
NSURLRequest *request = [requestQueue objectAtIndex:0];
currentConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)queueRequest:(NSURLRequest *)request delegate:(id)delegate
{
[requestQueue addObject:request];
[delegateQueue addObject:delegate];
if ([requestQueue count] == 1)
[self startNextConnection];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connection: connection didReceiveResponse: response];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connection: connection didReceiveData: data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connection: connection didFailWithError:error];
[currentConnection release];
currentConnection = nil;
[requestQueue removeObjectAtIndex:0];
[delegateQueue removeObjectAtIndex:0];
if ([requestQueue count] >= 1)
[self startNextConnection];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connectionDidFinishLoading: connection];
[currentConnection release];
currentConnection = nil;
[requestQueue removeObjectAtIndex:0];
[delegateQueue removeObjectAtIndex:0];
if ([requestQueue count] >= 1)
[self startNextConnection];
}
#end
To use the connection queue, create an NSURLRequest instance and then call:
[[ConnectionQueue sharedInstance] queueRequest:request delegate:self];
There's no need to create the singleton instance of ConnectionQueue explicitly. It will be created automatically. However, to properly clean up, you should call [ConnectionQueue releaseShared] when the application quits, e.g. from applicationWillTerminate: of your application delegate.
It's a really strange bug.
I'm use a code for download a file with NSURLConnection if the download finish , I have no leak.
But if I cancel the download, I have 1Mo memory not release.
I have make test with instrument, and the methode identified to create this leak is
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
is really strange
this is my code for create, manage and cancel download
Note: this download is a part of core data managed object, but I think the core data is not responsible of my leak.
- (void)loadURL:(NSURL *)url
{
if (currentLoadingDatas) { // just bool for prevent multiple download
return;
}
currentLoadingDatas = YES;
NSURLRequest *request = [[NSURLRequest alloc]
initWithURL: url
cachePolicy: NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval: 60
];
connectionDatas = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[request release];
}
#pragma mark NSURLConnection Delegates
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if (!self.transientData) {
self.transientData = [[NSMutableData alloc] init];
}
[self.transientData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.transientData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self willChangeValueForKey:#"datas"];
[self setValue:self.transientData forKey:#"datas"];
[self didChangeValueForKey:#"datas"];
[self.transientData release];
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
return nil;
}
-(void)cancelDownload
{
[self.connectionDatas cancel];
}
// fired by coreData
-(void)willTurnIntoFault
{
self.transientData = nil;
[self cancelDownload];
[super willTurnIntoFault];
}
// fired by coreData
-(void)didTurnIntoFault
{
[connectionDatas release];
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
[self.transientData release];
[super didTurnIntoFault];
}
Can you help me for identify the problem ?
Thanks a lot.
How is self.transientData property declared ?
Because you initialize with : self.transientData = [[NSMutableData alloc] init]; and if the property is set to retain the value, you will need to release it two times.
If so, to set the property use : self.transientData = [[[NSMutableData alloc] init] autorelease]; or simply [NSMutableData data];.
The rest seems ok to me.
The leak is indicating that your variable is instantiated at that point so that's not where the leak actually is that is where it starts. You need to release the transientData in your cancel method like this
- (void)cancelDownload
{
[self.connectionDatas cancel];
self.transientData = nil;
}
There are a few issues with inconsistency in your coding style that may make it harder for you to mentally track what is going. If you stick to your own standards it should be easier to follow the flow.
This is potentially where another leak could occur
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if (!self.transientData) {
self.transientData = [[NSMutableData alloc] init]; // leaky line
}
[self.transientData setLength:0];
}
The [[NSMutableData alloc] init] call is creating an instance of NSMutableData with a retain count of +1. Then if you are using properties (which are best) with retain then the self.transientData = will take another retain adding another +1 to the retain count. This is later released only once therefore you have a leak as the NSMutableData will still be hanging around.
Further down in your code you use the pattern
create instance and assign to local variable
assign instance to ivar
release local variable instance
This is the pattern I would use when I am not in an init method. Therefore the method before should be changed to:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if (!self.transientData) {
NSMutableData *tmpTransientData = [[NSMutableData alloc] init];
self.transientData = tmpTransientData;
[tmpTransientData release];
}
[self.transientData setLength:0];
}
This has the benefit of not using an autorelease so you are more explicit about when the object is no longer required which is preferred when possible on devices with small memory.
Another inconsistency which might benefit being tidied up a little is how you release your ivars. You have interchangeably done this in your code
[self.transientData release];
self.transientData = nil;
I would use the latter in my code (that is not in dealloc) for instance variables as the synthesized setter will call release for you and set the pointer to nil which is considerably safer.