I'm trying to run a download in a background thread as to not block the main UI thread on iOS, what I did was create a new thread using
[NSThread detachNewThreadSelector:#selector(startDownload) toTarget:downloadObject withObject:nil];
Then the following code runs on a background thread:
NSURL* urlForCalendar = [NSURL URLWithString:#"http://www.apple.com/"];
urlRequest = [NSURLRequest requestWithURL:urlForCalendar];
urlConnection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self startImmediately:NO];
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
[urlConnection scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[urlConnection start];
However, the delegate callbacks are never called.
EDIT:
For anyone who might come across a similar problem in the future, after a bit of trying to figure out why it wasn't working, I wasn't running the loop. So the last 3 lines of code should actually be:
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
[urlConnection scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[urlConnection start];
[runLoop run];
You don't run the run loop of the thread you created, so the connection you add to the run loop is never serviced and you never get any callbacks.
Generally you just want to handle the callbacks on the main thread and then push the result to a background thread if heavy processing is required.
You can do what you're currently doing though so long as you run the run loop and tidy up properly once the download is complete.
Related
First of all, I don't think that this question already exists, but I agree there is similar posts, please keep reading.
My question is: how abort a NSUrlConnection in his "is connecting" state? I mean, after the connection is made, we can use NSUrlConnection cancel method to cancel requests. But how abort it in "connecting" state, before it reaches the timeout when the server doesn't provide a response (before receiving any delegate calls)?
Thanks for your time!
EDIT
Should I use NSURLSession​Task instead of NSUrlConnection to do that (with its method cancel)?
EDIT 2 - code sample
NSURLConnection* m_connection;
m_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if(m_connection){
[m_connection start];
m_timer = [NSTimer scheduledTimerWithTimeInterval: FLT_MAX
target: self selector: #selector(doNothing:)
userInfo: nil repeats:YES];
m_runLoop = [NSRunLoop currentRunLoop];
[m_runLoop addTimer:m_timer forMode:NSDefaultRunLoopMode];
while (m_bRunLoop && [m_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
[m_connection cancel];
}
I use my connection to stream data. As you can see, for now I abort my connection setting m_bRunLoop to false and it's ok. But my question is : how abort my connection before the server send an response, without wait for the entire timeout?
You can still call [NSURLConnection cancel] to cancel connection and have no further delegate calls. Remember that you have to create a new connection object if you want to reconnect. From your question I deduce rather that you have a problem how to make this cancel call before receiving any delegate calls, is that the case?
Also, consider using NSURLSession API with data tasks as this is probably better way to handle networking in most cases.
EDIT (as you added code):
First of all, note that adding a timer to your run loop doesn't change anything in here as NSTimer is not considered as input source for the run loop (if you really "do nothing").
Second - if you set m_bRunLoop to false you do it somewhere, though you didn't provide the code - but this will be the place to cancel your connection, so let's name this place "cancelConnection" method.
Modify your code as follows:
m_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if(m_connection){
yourConnectionThread = [NSThread currentThread]; // store your thread in instance variable
[m_connection start];
m_timer = [NSTimer scheduledTimerWithTimeInterval: FLT_MAX
target: self selector: #selector(doNothing:)
userInfo: nil repeats:YES];
m_runLoop = [NSRunLoop currentRunLoop];
[m_runLoop addTimer:m_timer forMode:NSDefaultRunLoopMode];
}
and implement method where you cancel connection, remember that you need to call cancel on a thread on which you started connection:
- (void)cancelConnection {
//m_bRunLoop = false <- this was before here
[m_connection performSelector:#selector(cancel) onThread:yourConnectionThread withObject:nil waitUntilDone:YES];
}
Last, but rather as a comment - remember that NSRunLoop is not thread safe and you should not call it's methods from different threads.
I start a NSURLConnection in another thread:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
^{
NSURLConnection *connection = [NSURLConnection connectionWithRequest:[request preparedURLRequest] delegate:self];
[connection start];
});
But my delegate method is not called:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData*)data;
When run on the main thread everything is fine. How can I run connection on another thread and get the delegate methods called at the same thread too?
GCD creates, destroys, reuses threads implicitly and there is a chance that the thread you call start from will stop existing immediately afterwards. This may result in the delegate not receiving any callbacks.
If you would like to receive callback in background thread, you can use setDelegateQueue or sendAsynchronousRequest:queue:completionHandler: method:
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:NO];
[connection setDelegateQueue:[[NSOperationQueue alloc] init]];
[connection start];
The easiest way to start NSURLConnection in the background thread via GCD is:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSURLResponse* response = nil;
NSError* error = nil;
[NSURLConnection sendSynchronousRequest:request] returningResponse:&response error:&error];
NSLog(#"%#", response);
});
Yes, this is well known behavior of NSURLConnection because it needs a run loop to process the delegate events. The most common solution is (a) instantiate it with initWithRequest:delegate:startImmediately: where startImmediately is FALSE; (b) manually scheduleInRunLoop:forMode: to schedule it in the main run loop; and then (c) start the connection.
But, as you have it here, there's no point in dispatching this to a background queue, as it's already asynchronous so you should just initiate this from the main queue and none of the above is necessary. You use the above pattern in special cases (e.g. you were using NSOperation subclass to manage your requests), but generally it's not needed.
Also, FYI, effective iOS9, NSURLConnection is deprecated, so you should be using NSURLSession, anyway. And NSURLSession doesn’t suffer this limitation.
I had a similar issue. What I'm doing now is running NSURLConnection request in the main thread - it is running asynchronously so it won't slow down your application. In connectionDidFinishLoading, I run the following code to process the results of my calls. I perform the check because I have NSURLConnection call which may trigger other network calls. Since they are already running on a background thread I don't want to start a new one.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if ([NSThread isMainThread]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//Background Thread
[self processFinishLoading:connection];
});
}
else {
[self processFinishLoading:connection];
}
}
(UPDATED) this is the problem in a nutshell: in iOS I want to read a large file, do some processing on it (in this particular case encode as Base64 string() and save to a temp file on the device. I set up an NSInputStream to read from a file, then in
(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
I'm doing most of the work. For some reason, sometimes I can see the NSInputStream just stops working. I know because I have a line
NSLog(#"stream %# got event %x", stream, (unsigned)eventCode);
in the beginning of (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode and sometimes I would just see the output
stream <__NSCFInputStream: 0x1f020b00> got event 2
(which corresponds to the event NSStreamEventHasBytesAvailable) and then nothing afterwards. Not event 10, which corresponds to NSStreamEventEndEncountered, not an error event, nothing! And also sometimes I even get a EXC_BAD_ACCESS exception which I have no idea at the moment how to debug. Any help would be appreciated.
Here is the implementation. Everything starts when I hit a "submit" button, which triggers:
- (IBAction)submit:(id)sender {
[p_spinner startAnimating];
[self performSelector: #selector(sendData)
withObject: nil
afterDelay: 0];
}
Here is sendData:
-(void)sendData{
...
_tempFilePath = ... ;
[[NSFileManager defaultManager] createFileAtPath:_tempFilePath contents:nil attributes:nil];
[self setUpStreamsForInputFile: [self.p_mediaURL path] outputFile:_tempFilePath];
[p_spinner stopAnimating];
//Pop back to previous VC
[self.navigationController popViewControllerAnimated:NO] ;
}
Here is setUpStreamsForInputFile called above:
- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath {
self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
[p_iStream setDelegate:self];
[p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[p_iStream open];
}
Finally, this is where most logic occurs:
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
NSLog(#"stream %# got event %x", stream, (unsigned)eventCode);
switch(eventCode) {
case NSStreamEventHasBytesAvailable:
{
if (stream == self.p_iStream){
if(!_tempMutableData) {
_tempMutableData = [NSMutableData data];
}
if ([_streamdata length]==0){ //we want to write to the buffer only when it has been emptied by the output stream
unsigned int buffer_len = 24000;//read in chunks of 24000
uint8_t buf[buffer_len];
unsigned int len = 0;
len = [p_iStream read:buf maxLength:buffer_len];
if(len) {
[_tempMutableData appendBytes:(const void *)buf length:len];
NSString* base64encData = [Base64 encodeBase64WithData:_tempMutableData];
_streamdata = [base64encData dataUsingEncoding:NSUTF8StringEncoding]; //encode the data as Base64 string
[_tempFileHandle writeData:_streamdata];//write the data
[_tempFileHandle seekToEndOfFile];// and move to the end
_tempMutableData = [NSMutableData data]; //reset mutable data buffer
_streamdata = [[NSData alloc] init]; //release the data buffer
}
}
}
break;
case NSStreamEventEndEncountered:
{
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
stream = nil;
//do some more stuff here...
...
break;
}
case NSStreamEventHasSpaceAvailable:
case NSStreamEventOpenCompleted:
case NSStreamEventNone:
{
...
}
}
case NSStreamEventErrorOccurred:{
...
}
}
}
Note: when I posted this first, I was under a wrong impression the issue had something to do with using GCD. As per Rob's answer below I removed the GCD code and the issue persists.
First: in your original code, you were not using a background thread, but the main thread (dispatch_async but on the main queue).
When you schedule NSInputStream to run on the default runloop (so, the runloop of the main thread), the events are received when the main thread is in the default mode (NSDefaultRunLoopMode).
But: if you check, the default runloop changes mode in some situations (for example, during an UIScrollView scroll and some other UI updates). When the main runloop is in a mode different than the NSDefaultRunLoopMode, your events are not received.
Your old code, with the dispatch_async, was almost good (but move the UI Updates on the main thread). You have to add only few changes:
dispatch in the background, with something like this:
:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^{
// your background code
//end of your code
[[NSRunLoop currentRunLoop] run]; // start a run loop, look at the next point
});
start a run loop on that thread. This must be done at the end (last line) of the dispatch async call, with this code
:
[[NSRunLoop currentRunLoop] run]; // note: this method never returns, so it must be THE LAST LINE of your dispatch
Try and let me know
EDIT - added example code:
To be more clear, I copy-paste your original code updated:
- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath {
self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
[p_iStream setDelegate:self];
// here: change the queue type and use a background queue (you can change priority)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^ {
[p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[p_iStream open];
// here: start the loop
[[NSRunLoop currentRunLoop] run];
// note: all code below this line won't be executed, because the above method NEVER returns.
});
}
After making this modification, your:
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {}
method, will be called on the same thread where you started the run loop, a background thread: if you need to update the UI, it's important that you dispatch again to the main thread.
Extra informations:
In my code I use dispatch_async on a random background queue (which dispatch your code on one of the available background threads, or start a new one if needed, all "automagically"). If you prefer, you can start your own thread instead of using a dispatch async.
Moreover, I don't check if a runloop is already running before sending the "run" message (but you can check it using the currentMode method, look NSRunLoop reference for more informations). It shouldn't be necessary because each thread has only one associated NSRunLoop instance, so sending another run (if already running) does nothing bad :-)
You can even avoid the direct use of runLoops and switch to a complete GCD approach, using dispatch_source, but I've never used it directly so I can't give you a "good example code" now
NSStream requires a run loop. GCD doesn't provide one. But you don't need GCD here. NSStream is already asynchronous. Just use it on the main thread; that's what it's designed for.
You're also doing several UI interactions while on a background thread. You can't do that. All UI interactions have to occur on the main thread (which is easy if you remove the GCD code).
Where GCD can be useful is if reading and processing the data is time consuming, you can hand that operation to GCD during NSStreamEventHasBytesAvailable.
I am using NSUrlConnection for making http requests. I want to avoid event driven code, so I am making use of NSRunloop in the following way:
NSURLRequest *request = [[NSURLRequest alloc]
initWithURL: [NSURL URLWithString:_urlString]
cachePolicy: NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval: 10
];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(!connection)
{
DebugLog(#"Creating a connection has failed");
[self setValidationRequestResult:false];
}
else
{
NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
[self.connection start];
while (self.isConnectionComplete == NO)
{
NSDate* dateLimit = [NSDate dateWithTimeIntervalSinceNow:0.05];
[currentRunLoop runUntilDate:dateLimit];
}
//TODO: check release of NSUrlConnection
}
}
// Now perform remaining tasks ........
Is it okay to NSRunLoop in way shown above or should we post notifications in "didFailWithError" and "connectionDidFinishLoading" to write logic after the http request is done?
While that approach may be technically correct, I would encourage you to ask why you want to do this.
NSURLConnection was designed to attach itself to a runloop so the runloop can continue and not tie up that thread.
What I've done in my applications is have a class dedicated to managing my networking code and have delegate callbacks or pass in blocks to handle completion. You mentioned using Notifications and that is also a good idea.
This is the nature of asynchronous networking code. It makes the code a bit more complicated, but your application will be better off.
How can I get NSURLConnection to call it's delegate methods from a different thread instead of the main thread. I'm trying to mess around with the scheduleInRunLoop:forMode:but doesn't seem to do what I want.
I have to download a large file and it interrupts the main thread so frequently that some rendering that is happening starts getting choppy.
NSURLRequest * request = [NSURLRequest requestWithURL:url];
NSURLConnection * connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
NSRunLoop * loop = [NSRunLoop currentRunLoop];
NSLog(#"loop mode: %#",[loop currentMode]);
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
The other thing I don't see much of is "Modes" There are only two modes documented so not much really to test with.
Any ideas?
Thanks
There are several options:
In your implementation of the delegate methods, make use of dispatch_async.
Start the schedule the connection on a background thread.
You can do the latter like this:
// all the setup comes here
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
[connection scheduleInRunLoop:loop forMode:NSRunLoopCommonModes];
[loop run]; // make sure that you have a running run-loop.
});
If you want a guarantee on which thread you're running, replace the call to dispatch_get_global_queue() appropriately.
If you want to perform downloads on a separate thread, I'm pretty sure these are the droids you're looking for...
- (void) dispatchRequest{
self->finished = NO;
NSMutableURLRequest* request = //Formulate your request
NSThread* download_thread = [[NSThread alloc] initWithTarget:self selector:#selector(downloadThreadLoop:) object:request];
[download_thread start];
}
- (void) downloadThreadLoop:(NSMutableURLRequest*) request{
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
while(!self->finished]){
//This line below is the magic!
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
//...
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
//...
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
//...
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
//...
self->finished = YES;
}
If you truly need to do the download in a new thread, it may be easier to detachNewThreadSelector:toTarget:withObject:, setup (and destroy) an NSAutoreleasePool, and then use one of the synchronous selectors like NSData's dataWithContentsOfURL:. This will not make use of the asynchronous NSURLConnectionDelegate.
Because this call is synchronous, it will not return until the file has been downloaded, which will block the main thread, but because you're in a new thread, it won't. Please note that this is typically discouraged behavior. Is there other code happening in the main thread that can be optimized?
NSURLConnection is already doing the download off of the main thread asynchronously. If I understand your question, you would like the delegate messages to be sent on a thread other than the main thread? You can't do that as you can't modify the internal implementation of NSURLConnection. I can think of two ways to simulate this.
Create a sublcass of NSURLConnection (e.g. MyURLConnection) that assigns itself as own delegate. Note that this creates an intentional retain cycle so be careful. MyURLConnection should define a new delegate that supports NSURLConnectionDelegate. Let's call this finalDelegate. When MyURLConnection handles it's own delegate messages, forward or dispatch them to finalDelegate on whatever thread you like.
Similar to option #1 but without the subclass. Handle the NSURLConnection delegate methods on the main thread and forward/dispatch them to whatever thread you like.
The main difference is if you want a reusable subclass that behaves this way or it's a one off implementation.
EDIT: added suggestion on how to run code in the background
If you are going to process the response in the background I would either use operations or grand central dispatch. No need to mess around with run loops and creating threads. Check out Apple's Concurrency Programming Guide.