I use a NSThread in order to download videos and images from a server side.It work looks and works great except the fact that when the downloading is done my GUI gets blocked until the download is complete.When the download is finished it takes a few seconds to work again.
this is how the server request is done:
- (void) repeatRequest{
NSLog(#"repeatRequest");
[NSThread detachNewThreadSelector:#selector(backgroundRequest) toTarget:self withObject:nil];
}
- (void) backgroundRequest{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSURL *url = [NSURL URLWithString:myURLStr];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
[pool drain];
}
- (void)requestFinished:(ASIHTTPRequest *)request
{
//do things
}
IMPORTANTAnd I also tried to start the ASIHTTPRequest from the GUI thread but with the same behaviour.
Any idea about what could be wrong?
EDIT:
- (void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[[UIApplication sharedApplication] setStatusBarHidden:YES];
//internetReachable = [[Reachability reachabilityForInternetConnection] retain];
if(timer1 == nil)
{
timer1 = [NSTimer scheduledTimerWithTimeInterval:60.0 target:self selector: #selector(repeatRequest) userInfo: nil repeats: YES];
}
}
Try to run synchronous ASIHTTPRequest in your background thread, and handle results not in delegate method (requestFinished), but after [request startSynchronous];
I don't know anything about ASIHTTPRequest but i would assume its -startAsynchronous method already handles the background downloading for you. It all likelihood, it is returning immediately and your new thread is exiting. Also, you should just use [pool release] at the end of a thread method instead of [pool drain], it will be drained upon release, and you won't be leaking an NSAutoReleasePool. Does ASIHTTPRequest have a -startSynchronous (or just plain -start) method? Try using that within -backgroundRequest, as it should block the premature exit of that thread.
Related
I have some code that sends multiple ASIHTTPRequests to upload and download data in a view controller. When the view controller gets dealloc'd it should clean up all unfinished requests by setting the delegate to nil.
- (void)viewDidLoad
{
// send multiple requests
[self sendRequest:someURL];
[self sendRequest:someURL];
[self sendRequest:someURL];
[self sendRequest:someURL];
}
- (void)sendRequest:(NSString*)url
{
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
ASINetworkQueue *requestQueue = [ASINetworkQueue queue];
[requestQueue setMaxConcurrentOperationCount:2];
[requestQueue setDelegate:self];
[requestQueue addOperation:request];
[requestQueue go];
}
- (void)dealloc
{
NSLog(#"cancel all operations");
for (ASIHTTPRequest *req in ASIHTTPRequest.sharedQueue.operations)
{
[req cancel];
[req setDelegate:nil];
}
[super dealloc];
}
However, if I pop this view controller before all operations have finished, I get a "message sent to deallocated instance" in ASIHTTPRequest.m complaining that the delegate went away in the code below.
/* ALWAYS CALLED ON MAIN THREAD! */
- (void)reportFailure
{
***crash here --> if (delegate && [delegate respondsToSelector:didFailSelector]) {
[delegate performSelector:didFailSelector withObject:self];
}
if (queue && [queue respondsToSelector:#selector(requestFailed:)]) {
[queue performSelector:#selector(requestFailed:) withObject:self];
}
#if NS_BLOCKS_AVAILABLE
if(failureBlock){
failureBlock();
}
#endif
}
How can I work around this?
You're creating a new queue for each request around this line of code:
ASINetworkQueue *requestQueue = [ASINetworkQueue queue];
So the loop here won't loop over the requests as it's looping over the sharedQueue, not the new one(s) you've created:
for (ASIHTTPRequest *req in ASIHTTPRequest.sharedQueue.operations)
Requests would only get added to the sharedQueue if you use [request startAynchronous] without explicitly setting a different queue.
I may be missing something, but I think waiting until dealloc is too late, you want to cancel your operations on viewWillDisappear or viewDidUnload
Kinda stuck on this problem and I'm not sure, where I've gone wrong. Heres what I'm doing:
Class calls:
- (void)updateApplicationDataInBackground {
updateView = [[UpdatingView alloc] init];
[self.view addSubview:updateView.view];
DataSynchronizer *dataSynchronizer = [[DataSynchronizer alloc] init];
[NSThread detachNewThreadSelector:#selector(initWithDataRequest:) toTarget:dataSynchronizer withObject:self];
[dataSynchronizer release];
This creates a thread to retrieve data from the server and parse it. In DataSynchronizer this is the method being called:
- (void)initWithDataRequest:(id)parent {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
controller = parent;
NSLog(#"DataSynchronizer initWithDataRequest called");
NSURL *url = [NSURL URLWithString: ApiUrl];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:ApiKey forKey:#"key"];
[request setPostValue:ApiPass forKey:#"password"];
[request setPostValue:#"somevalue" forKey:#"framework"];
[request setPostValue:#"somevalue" forKey:#"method"];
[request setDidFinishSelector:#selector(parseResult:)];
[request setDidFailSelector:#selector(requestError:)];
[request setTimeOutSeconds:60];
[request setDelegate:self];
[request startAsynchronous];
[pool release];
After my data is received I parse the contents and do my data synch. This is all working as expected. I've decided to throw in a UIProgressView so the user can see what is going on with this request, this progress view lives in updateView which is created in the updateApplicationDataInBackground.
I'm not trying to show progress for the web service call but simply when milestones are reached in the data processing. In the DidFinishSelector its calling parseResult
There are five method its calls with the response data:
[self parseData:[data objectForKey:#"types"] forObject:[Types class] andParent:nil];
[controller performSelectorOnMainThread:#selector(updateProgress:) withObject:[NSNumber numberWithFloat:.4] waitUntilDone:YES];
After each process I'm trying to update the UIProgressView, it will never update. Now if I simply call performSelectorOnMainThread from outside the ASIHTTPRequest it works as expected, but not within the DidFinishSelector. I've tried many variations on this where it calls a local method which updates the mainThread, where I simply use performSelector. Nothing works, how do I update the the UIProgessView?
Is the problem a thread spawning a thread?
Thanks
EDIT:
Looks like the DidFinishSelector is being called on the main thread already. I've updated my code to simply call:
[controller updateProgress:[NSNumber numberWithFloat:.8]]
Still no luck....
Realized it might be helpful to see the UIProgessView update method.
- (void)updateProgress:(NSNumber *)progress {
float newProgess = [progress floatValue];
[updateView.myProgress setProgress: newProgess];
Ok so it looks like I found my own answer after changing somethings around. Because ASIHttpRequest performs SetDidFinish selector on the main thread my calls performSelectorOnMainThread weren't doing anything. I changed my initial call for the DataSynchronizer to the main thread and added changed the DidFinish method to:
- (void)parseDataInBackground:(ASIHTTPRequest *)request {
[NSThread detachNewThreadSelector:#selector(parseResult:) toTarget:self withObject:request];
Which then makes the parse method run on separate thread (since its the bulk of the processing and now performOnMainThread works without issue.
I'm writing test cases for a wrapper class written around ASIHTTPRequest. For reasons I can't determine, my test cases complete with failure before the ASIHTTPRequest finishes.
Here's how the program flow works.
Start in my test case.
Init my http engine object, instruct it to create a new list
Create the new ASIHTTPRequest object and set it up.
Add the request to an operation queue.
Wait until that queue is empty
Check to see if my delegate methods were called and fail the test if they weren't.
Now, most of the time everything works fine and the test passes, but some of the time it fails because my delegate methods were called AFTER the operation queue returned control to my wait method.
Test Case
// Set my flags to 'NO'
- (void)setUp {
requestDidFinish = NO;
requestDidFail = NO;
}
- (void)testCreateList {
NSString *testList = #"{\"title\": \"This is a list\"}";
JKEngine *engine = [[JKEngine alloc] initWithDelegate:self];
NSString *requestIdentifier = [engine createList:jsonString];
[self waitUntilEngineDone:engine];
NSString *responseString = responseString_;
[engine release];
GHAssertNotNil(requestIdentifier, nil);
GHAssertTrue(requestDidFinish, nil);
GHAssertTrue([responseString hasPrefix:#"{\"CreateOrEditListResult\""], nil);
}
// Puts the test into a holding pattern until the http request is done
- (void)waitUntilEngineDone:(JKEngine *)engine {
[engine waitUntilFinishedRunning];
}
// The delegate method called on successful completion
- (void)requestFinished:(NSString *)requestIdentifier withResponse:(NSString *)response {
NSLog(#"request did finish");
requestDidFinish = YES;
responseIdentifier_ = [requestIdentifier retain];
responseString_ = [response retain];
}
Engine Code
- (NSString *)createList:(NSString *)list {
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:url]];
[request addRequestHeader:#"Content-Type" value:kContentType];
[request setRequestMethod:kPOST];
request.delegate = self;
[request appendPostData:[list dataUsingEncoding:NSUTF8StringEncoding]];
NSString *requestIdentifier = [NSString stringWithNewUUID];
[operationQueue_ addOperation:request];
[operationDictionary_ setObject:request forKey:requestIdentifier];
return requestIdentifier;
}
// This is the ASIHTTPRequest delegate method that's called on success
// but it sometimes isn't called until AFTER the operationQueue finishes running
- (void)requestFinished:(ASIHTTPRequest *)request {
DLog([request responseString]);
BOOL canNotifiyDelegate = [self.delegate respondsToSelector:#selector(requestFinished:withResponse:)];
if (canNotifiyDelegate) {
NSArray *keyArray = [operationDictionary_ allKeysForObject:request];
NSString *requestIdentifier = [keyArray objectAtIndex:0];
[operationDictionary_ removeObjectForKey:requestIdentifier];
if ([keyArray count] != 1) {
ALog(#"It looks like a request was added to the operation dictionary multiple times. There's a bug somewhere.", nil);
}
[self.delegate requestFinished:requestIdentifier withResponse:[request responseString]];
}
}
- (void)waitUntilFinishedRunning {
[operationQueue_ waitUntilAllOperationsAreFinished];
}
This is the way ASIHTTPRequest works. Delegate methods are called on the main thread, and calls to delegates do not block the request thread, so it's perfectly possible your delegates will be called after the queue finishes.
ASIHTTPRequest calls delegate methods on the main thread, by default GH-Unit runs its tests on a background thread. I'm still a little hazy on exactly what was going on, but forcing my network tests to run on the main thread fixed the problem.
I implemented the following method in my network test class.
- (BOOL)shouldRunOnMainThread {
return YES;
}
Hope you guys can help me :)
In the main thread, I create a NSOperation and add it to a queue.
What that operation do is connect to a data server with NSURLConnection, save the receivedData and parse it.
Operation.m
- (void)start
{
NSLog(#"opeartion for <%#> started.", [cmd description]);
[self willChangeValueForKey:#"isExecuting"];
_isExecuting = YES;
[self didChangeValueForKey:#"isExecuting"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url];
[request setHTTPMethod:#"POST"];
[request setValue:[NSString stringWithFormat:#"multipart/form-data; boundary=%#", m_BOUNDARY] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:_postData];
_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (_connection == nil)
[self finish];
}
Then in this NSURL delegate method I parse the data I've just received from server.
Operation.m
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self parseItems];
}
In the data, I can found items like, for instance, screenItem, CellItem, TextItem that I send to the main thread for drawing them while arriving. (I create a UITableView if an itemTable arrives, or I create a UIWebView if an itemWeb arrives)
Using this for sending item to main thread:
Operation.m
- (void) parseItems
{
while ([_data length] > 0)
{
NSInteger type = [self _readByte];
switch (type)
{
case SCREEN:
{
[self _send: [self _readScreen]];
break;
}
case CELL:
{
[self _send: [self _readCell]];
break;
}
// ... A lot of different items
}
}
}
- (void)_send:(CItem*)_item
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"newItem" object:_item];
}
Then in notification receiver:
AppDelegate.m
- (void) _newItemArrived:(NSNotification *) notification
{
[self performSelectorOnMainThread:#selector(processItem:) withObject:[notification object] waitUntilDone:NO];
}
My problem is that the UI is not painted until NSOperation finish. I thought that NSOpertion, being a different thread, would not block the main thread, but believe that is what is happening.
Some tips for this issue?
Thanks a lot for reading!
Are you using NSOperationQueue?
Check out this answer to the question NSOperation blocks UI painting? for a simple example of how to update the UI with a notification from an NSOperation running asynchronously on another thread.
UPDATE
NSURLConnection supports asynchronous connections by itself with a delegate. You should use this. If you have specific issue(s), you should describe those.
Check out the ASIHTTPRequest library.
If you really want to use this approach, you could trying running NSURLConnection synchronously (using the class method sendSynchronousRequest:returningResponse:error:). Your app would remain responsive since the connection is on a background thread. However, you would not be able to update anything until all the data is received.
So I know this is a pretty old question but I ran into the same issue and after hours of going through documentation and blogs I found a great solution in this post from Wim Haanstra http://www.depl0y.com/?p=345
Putting your NSOperation in an infinite loop until you get data back should do the trick!
I have the following methods in my class:
-(IBAction)loginToAccount:(id)sender {
// Display the network activity status indicator
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
// Show the load indicator
[self.loadIndicator startAnimating];
self.loadIndicator.hidden = NO;
self.loadLabel.hidden = NO;
[usernameTextField resignFirstResponder];
[passwordTextField resignFirstResponder];
[self CheckLoginCredentials];
}
-(void)CheckLoginCredentials {
NSString *APIURL = [[NSString alloc] initWithFormat:#"http://mysite.com/xml.xml"];
NSURL *url = [[NSURL alloc] initWithString:APIURL];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
[APIURL release];
[url release];
[xmlParser setDelegate:self];
[xmlParser parse];
}
When I comment [self CheckLoginCredentials], the loadIndicator gets animated and shown but when I uncomment [self CheckLoginCredentials], the loadIndicator does not get shown and also usernameTextField/passwordTextField resignFirstResponder do no work.
What am I doing wrong? Thanks!
I believe that -initWithContentsOfURL: is a synchronous url connection, and therefore blocks the thread it's run on until it completes.
Because of this, the progress indicator won't get shown because it requires that the thread it's running on has an active run loop. Using the synchronous url connection on the main thread will block the UI on that thread, so you won't see your progress indicator.
The correct way to do this would be to use NSURLConnection's
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id)delegate
Simply create an NSURLRequest object that encapsulates your API request, and then pass it off to that method on NSURLConnection.
Then implement the delegate call backs to get your data back.
The advantage of this method is that all of this is done on a separate thread and handled for you, and therefore won't block your UI.