4

I want to do an Asynchrous NSURLConnection inside of an NSOperation on a background thread. it is because I'm doing some very expensive operations on the data as they come back.

This is a very similar question to what they asked here: How do I do an Asynchronous NSURLConnection inside an NSOperation?

but the difference is that I run the connection in another class.

Here is my first attempt:

In my MainViewController:

@property (nonatomic, strong) NSOperationQueue *requestQueue;

#pragma mark - Lazy initialization
- (NSOperationQueue *)requestQueue 
{
    if (!_requestQueue) {
        _requestQueue = [[NSOperationQueue alloc] init];
        _requestQueue.name = @"Request Start Application Queue";
        _requestQueue.maxConcurrentOperationCount = 1;
    }
    return _requestQueue;
}

-(void)callToServer
{
URLJsonRequest *request = [URLRequestFactory createRequest:REQUEST_INTERFACE_CLIENT_VERSION
                                                         delegate:self];

    RequestSender *requestSender = [[RequestSender alloc]initWithPhotoRecord:request delegate:self];

   [self.requestQueue addOperation:requestSender];
}

Here is my operation:

- (id)initWithPhotoRecord:(URLJsonRequest *)request
                 delegate:(id<RequestSenderDelegate>) theDelegate{

    if (self = [super init])
    {
        self.delegate = theDelegate;
        self.jsonRequest = request;
    }
    return self;
}

- (void)main {

    //Apple recommends using @autoreleasepool block instead of alloc and init NSAutoreleasePool, because blocks are more efficient. You might use NSAuoreleasePool instead and that would be fine.
    @autoreleasepool
    {

        if (self.isCancelled)
            return;

        [self.jsonRequest start];

    }
}

Here is my Request start function:

-(void) start
{
  NSURL *url = [NSURL URLWithString:@"http://google.com"];
 NSURLRequest *theRequest = [NSURLRequest requestWithURL:url];
  urlConnection = [[[NSURLConnection alloc]    initWithRequest:theRequest delegate:self]autorelease];

[urlConnection start];
[theRequest release]
}


- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@"Received reponse from connection");
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{



}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection{

}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

}

I do not get a response from the server.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Guy Kahlon
  • 4,510
  • 4
  • 30
  • 41
  • By the way, if you use `initWithRequest` without the `startImmediately` option set to `NO`, you shouldn't `start` the connection (because it will otherwise automatically be started, and issuing another `start` will interfere). Given that you need to schedule it in a run loop, use the rendition with `startImmediately` set to `NO`, schedule it in a run loop, and then `start` it. – Rob Jul 02 '13 at 13:47
  • Maybe I'm overlooking something, but I also don't see you doing the necessary KVN notification for `isExecuting` and `isFinished` that you'd expect to see when [subclassing your own operation](http://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW16). Or are you not supporting concurrent operation? – Rob Jul 02 '13 at 14:59
  • You only want a subclass of `NSOperation` if you want to control the maximum number of simultaneous active connections. Otherwise, subclassing from `NSOperation` is pointless. The `NSURLConnection` won't run on the queue's execution context anyway - except for the start method. What's important is where your delegate methods will be executed. Using a dedicated thread is one way, dispatching the work (processing the chunks) to another queue is another. Both approaches have pros and cons. – CouchDeveloper Jul 02 '13 at 22:28

3 Answers3

2
-(void)start
{
  [self willChangeValueForKey:@"isExecuting"];
  _isExecuting = YES;
  [self didChangeValueForKey:@"isExecuting"];
  NSURL* url = [[NSURL alloc] initWithString:@"http://url.to/feed.xml"];
  NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20];
  _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; // ivar
  [request release];
  [url release];
  // Here is the trick
  NSPort* port = [NSPort port];
  NSRunLoop* rl = [NSRunLoop currentRunLoop]; // Get the runloop
  [rl addPort:port forMode:NSDefaultRunLoopMode];
  [_connection scheduleInRunLoop:rl forMode:NSDefaultRunLoopMode];
  [_connection start];
  [rl run];
}

More details can be found here: link

torip3ng
  • 585
  • 2
  • 7
  • Do you know the solution to speak about in this link? http://www.russellj.co.uk/blog/2011/07/09/nsurlconnection-in-a-background-thread/#comment-168 if you can expand a little on the differences between the two solutions. – Guy Kahlon Jul 22 '13 at 11:43
2

A couple of approaches:

  1. Schedule the NSURLConnection in the main run loop, by using the startImmediately parameter of NO, set the run loop, and only then should you start the connection, e.g.:

    urlConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self startImmediately:NO];
    [urlConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    [urlConnection start];
    
  2. Create a dedicated thread for the the connection and schedule the connection in the run loop you create for that thread. See AFURLConnectionOperation.m in AFNetworking source for an example of this.

  3. Actually use AFNetworking, which gives you NSOperation based operations that you can add to your queue, and takes care of this run loop stuff for you.


So, AFNetworking does something like:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"NetworkingThread"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self
                                                        selector:@selector(networkRequestThreadEntryPoint:)
                                                          object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

So I do something like the following. First I have a few private properties:

@property (nonatomic, readwrite, getter = isExecuting)  BOOL executing;
@property (nonatomic, readwrite, getter = isFinished)   BOOL finished;
@property (nonatomic, weak)   NSURLConnection *connection;

Then the network operation can then do something like:

@synthesize executing = _executing;
@synthesize finished  = _finished;

- (instancetype)init {
    self = [super init];
    if (self) {
        _executing = NO;
        _finished = NO;
    }
    return self;
}

- (void)start {
    if (self.isCancelled) {
        [self completeOperation];
        return;
    }

    self.executing = YES;

    [self performSelector:@selector(startInNetworkRequestThread)
                 onThread:[[self class] networkRequestThread]
               withObject:nil
            waitUntilDone:NO];
}

- (void)startInNetworkRequestThread {
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:self.request
                                                                  delegate:self
                                                          startImmediately:NO];
    [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [connection start];

    self.connection = connection;
}

- (void)completeOperation {
    self.executing = NO;
    self.finished = YES;
}

- (void)setFinished:(BOOL)finished {
    if (finished != _finished) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

- (void)setExecuting:(BOOL)executing {
    if (executing != _executing) {
        [self willChangeValueForKey:@"isExecuting"];
        _executing = executing;
        [self didChangeValueForKey:@"isExecuting"];
    }
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isAsynchronous {
    return YES;
}

// all of my NSURLConnectionDataDelegate stuff here, for example, upon completion:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    // I call the appropriate completion blocks here, do cleanup, etc. and then, when done:

    [self completeOperation];
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • @Rob.Option 2 is very suitable for me. Can I get from you another information on how to write thread that will contact the server and parse the information. thanks. – Guy Kahlon Jul 02 '13 at 14:17
  • @GuyKahlonMatrix I've updated my answer with sample code for performing the download in this operation. – Rob Jul 02 '13 at 14:59
  • do you know the solution to discuss about in this link? http://www.russellj.co.uk/blog/2011/07/09/nsurlconnection-in-a-background-thread/#comment-168 – Guy Kahlon Jul 22 '13 at 11:44
0

I know this post is over a year old, but I wanted to add some suggestions for people who may run the same problem trying to create own async network operation. You need to add runloop to operation that runs in background and you should stop it when the operations has finished.

There actually 2 simple options:

Option 1 - using NSRunLoop

NSPort *port        = [NSPort port];
NSRunLoop *runLoop  = [NSRunLoop currentRunLoop];
[runLoop addPort:port forMode:NSDefaultRunLoopMode];
[self.connection scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];
[self.connection start];
[runLoop run];

and you need to stop when the operation is finished:

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSDate *date       = [NSDate distantFuture];
while (!runLoopIsStopped && [runLoop runMode:NSDefaultRunLoopMode beforeDate:date]);

Option 2 - using CF

You need to add

CFRunLoopRun();

when you start operation

and call

CFRunLoopStop(CFRunLoopGetCurrent());

when you finish operation.

Read the following post: CFRunLoopRun() vs [NSRunLoop run]

Community
  • 1
  • 1
PiotrDomo
  • 1,045
  • 10
  • 29