Un problema con NSURLConnection y NSOperationQueue en iPhone OS 4.0

El Contexto

Las clases NSOperation y NSOperationQueue incluidas en los frameworks Cocoa y CocoaTouch proporcionan una forma simple y elegante de implementar operaciones concurrentes (programación multihilo o multithreaded).

La manera de utilizar estas clases es crear una subclase de NSOperation, sobre-escribir el método start y poner en él todo el código que se desee ejecutar concurrentemente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@interface OperacionMuyLarga : NSOperation {}
@end

@implementation OperacionMuyLarga
    - (id)init {
        self = [super init];
        if (self){
            self.executing = NO;
            self.finished = NO;
        }
        return self;
    }

    - (BOOL)isConcurrent {
        return YES;
    }

    - (void)start {
        [self willChangeValueForKey:@"isExecuting"];
        self.executing = YES;
        [self didChangeValueForKey:@"isExecuting"];

        // Operación muy larga

        [self willChangeValueForKey:@"isExecuting"];
        [self willChangeValueForKey:@"isFinished"];
        self.executing = NO;
        self.finished = YES;
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];
    }
@end

Luego se debe crear una instancia de la operación y añadirla ([NSOperationQueue addOperation:]) a una instancia de NSOperationQueue. Se puede crear un objeto singleton que herede de NSOperationQueue o mantener una instancia de NSOperationQueue en el application delegate, la idea es utilizar el mismo NSOperationQueue para todas las operaciones.

1
2
unaOperacion *operacion = [[OperacionMuyLarga alloc] init];
[queue addOperation:unaOperacion]; //queue es una instancia de NSOperationQueue

El Problema

Hasta aqui todo va bien pero cuando el código en //Operación muy larga es a su vez código asincrónico, es decir, inicia su(s) propio(s) hilos, este deja de funcionar.

El siguiente ejemplo de una operación que inicia una operación de comunicación por red utilizando NSURLConnection funciona bien en iPhone OS 3.0 pero deja de funcionar en iPhoneOS 4.0, los métodos delegados de NSURLRequest nunca son llamados.

1
2
3
4
5
6
7
8
9
10
11
- (void)start
{
    [self willChangeValueForKey:@"isExecuting"];
    self.executing = YES;
    [self didChangeValueForKey:@"isExecuting"];

    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}

// Aqui van los métodos delegados de NSURLRequest

Sospecho que Apple cambió la implementación del método [NSUrlConnection initWithRequest:] y cuando éste se llama desde un hilo que no es el principal, la descarga no se inicia inmediatamente y el hilo iniciado por NSOperationQueue (donde vive el método start y los métodos delegados) habrá terminado antes de que los métodos delegados sean llamados.

Una Solución Rápida

Pero que dudo sea la ideal es cerciorarse de que la conexión y sus métodos delegados se ejecuten desde el hilo principal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)start
{
    if (![NSThread isMainThread]){
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }

    [self willChangeValueForKey:@"isExecuting"];
    self.executing = YES;
    [self didChangeValueForKey:@"isExecuting"];

    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}

// Aqui van los métodos delegados de NSURLRequest

Una Solución Más Elegante

La que implementaré en proyectos futuros es utilizar la clase ASIHTTPRequest. Según su documentación, se puede utilizar desde el hilo principal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (IBAction)grabURLInBackground:(id)sender{
    NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request setDelegate:self];
    [request startAsynchronous];
}

- (void)requestFinished:(ASIHTTPRequest *)request{
    // Use when fetching text data
    NSString *responseString = [request responseString];

    // Use when fetching binary data
    NSData *responseData = [request responseData];
}

- (void)requestFailed:(ASIHTTPRequest *)request{
    NSError *error = [request error];
}

O utilizando un NSOperationQueue (ASIHTTPRequest es una subclase de NSOperation):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (IBAction)grabURLInTheBackground:(id)sender{
    NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(requestDone:)];
    [request setDidFailSelector:@selector(requestWentWrong:)];
    [queue addOperation:request]; //queue es una instancia de NSOperationQueue
}

- (void)requestDone:(ASIHTTPRequest *)request{
    NSString *response = [request responseString];
}

- (void)requestWentWrong:(ASIHTTPRequest *)request{
    NSError *error = [request error];
}

Nota al Pie

Una de las aplicaciones de un cliente de la compañía donde trabajo dejó de funcionar en los iPhones y iPods que actualizaron su sistema operativo a la versión 4.0. Este tipo de situaciones son una pesadilla para los desarrolladores y es difícil explicar a los clientes la razón por la que su aplicación dejó de funcionar. Este es uno de los problemas de trabajar con frameworks cerrados que cambian al antojo de su autor y no permiten que los desarrolladores lean el código se su implementación.

Por otro lado, iPhone es la palabra en boga de los últimos dos años, muchos clientes quieren una aplicación y Cocoa(Touch) es uno de los frameworks mejor diseñados que conozco.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>