Magazine Informatica

[Aggiornato] Chiamate asincrone con NSOperationQueue

Creato il 10 marzo 2010 da Malcommac

La programmazione multithread è un’operazione abbastanza delicata in qualunque linguaggio; se va male qualcosa di solito lo fa nella maniera peggiore possibile. Di solito si cerca sempre o di evitarla completamente se possibile (e difficilmente lo è al giorno d’oggi) o di testarne l’implementazione quanto più pesantemente, onde evitare le brutte soprese. Fortunatamente da OS X 10.5 in poi (e anche su Mac OS X), Apple ha introdotto alcune interessanti funzioni di gestione per la classe NSThread e ha aggiunto due nuovi oggetti: NSOperation e NSOperationQueue. Se si viene da Java la cosa più vicina a cui si possono associare è l’interfaccia Runnable; proprio come in Java l’oggetto NSOperation è disegnato per essere esteso ed implementare l’insieme di azioni che vanno eseguite in un threading; queste azioni vanno posizionate all’interno del metodo – (void) main {}. Il modo più facile per usare un oggetto NSOperation è quello di farlo eseguire da NSOperationQueue, che come è possibile intuire dal nome permette di mantenere una coda di azioni (oggetti NSOperation) che potranno essere anche esauriti in parallelo. Ecco un esempio tipico:

NSOperationQueue * queue = [[NSOperationQueue alloc] init];
NSOperation * myOperation = [[[MyOperationObject alloc] init] autorelease];
[queue addOperation: myOperation];

NSOperationQueue si occuperà di gestire la coda in maniera intelligente distribuendo al meglio il carico sull’hardware che c’è a disposizione. In questo post però non ci occuperemo di introdurre la gestione multithreading, ma analizzeremo un particolare caso che la riguarda: la possibilità di far gestire, all’interno di un NSOperation, un’operazione asincrona come può essere quella di NSURLConnection. Il problema in questa situazione è quello di mantenere vivo l’oggetto operation anche dopo che il codice all’interno del main è stato eseguito; una volta creato e fatto partire NSURLConnection (con delegato l’oggetto coda stesso) e terminato il codice del main, l’oggetto viene automaticamente “marcato” come eseguito e rimosso dal gestore; il risultato è che il nostro NSURLConnection non verrà mai realmente utilizzato. Come risolvere? NSOperation può lavorare in due modalità: concorrente e non concorrente. Generalmente una sottoclasse di NSOperation lavorerà, se non specificato, in modalità non concorrente (ovvero in parallelo); sebbene però la terminologia possa portare a confusione, le operazioni che vengono eseguite in parallelo sono definite non-concorrenti. Per avviare un’operazione in modalità concorrente è necesssario sovrascrivere il metodo – (BOOL) isConcurrent per ritornare YES:

- (BOOL)isConcurrent {
    return YES;
}

Il primo passo è quello di far partire il nostro NSOperationQueue sul thread primario e da li attivare la connessione asincrona (su un altro thread) con NSURLConnection; a questo punto dovremmo prenderci cura dell’oggetto NSOperation e far si che non venga rimosso dalla coda prima che il download dei dati sia completato. Per far partire NSOperation nel thread principale sarà necessario sovrascrivere il metodo -start:

- (void)start {
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:
           @selector(start) withObject:nil waitUntilDone:NO];
        return;
    }
[super start];
}
- (void) main {
    NSLog(@"our opeartion for %@ is now started.", _url);

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

    NSURLRequest * request = [NSURLRequest requestWithURL:_url];
    _connection = [[NSURLConnection alloc] initWithRequest:request
                   delegate:self];
    if (_connection == nil)
        [self finish];
}

Analizziamo il codice: nella prima parte abbiamo fatto si che il l’operazione fosse forzata ad essere avviata nel main loop principale; poi abbiamo notificato in una variabile creata appositamente il fatto che l’operazione di download è iniziata. Infine abbiamo fatto partire il download. La seconda parte consiste nel creare un metodo – (void) finish che si occuperà di porre fine alla coda una volta esaurito il compito del download:

- (void)finish {
    [_connection release];
    _connection = nil;

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

Il punto chiave è nelle variabili _isExecuting e _isFinished: soltanto quando sono rispettivamente NO e YES l’operazione potrà essere rimossa dala coda; i rispettivi valori sono monitorati tramite l’uso del key-value observing. Infine sarà necessario implementare il delegato di NSURLConnection che si occuperà di chiamare, dove necessario (quindi nei casi di download completo o fallito) il metodo finish.

- (void)connection:(NSURLConnection *)connection
 didReceiveResponse:(NSURLResponse *)response {
    [_data release];
    _data = [[NSMutableData alloc] init];

    NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
    _statusCode = [httpResponse statusCode];
}

- (void)connection:(NSURLConnection *)connection
 didReceiveData:(NSData *)data {
    [_data appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self finish];
}

- (void)connection:(NSURLConnection *)connection
 didFailWithError:(NSError *)error {
    _error = [error copy];
    [self finish];
}

Come abbiamo visto non è stato necessario implementare le API di download in modo sincrono; questa tecnica ci ha permesso di pacchettizzare facilmente all’interno di una coda una funzione asincrona. Una funzione del genere può essere particolarmente utile quando si ha a che fare con la gestione di più download simultaneamente ed è necessario monitorarne lo stato (cosa che non sarebbe possibile con una normale chiamata sincrona) o mantenere un massimo di attività contemporaneamente (facendo uso di -(void) setMaxConcurrentOperationCount: su NSOperationQueue). Cliccando qui potrete scaricare la classe RFDataDownloader (1.0.1) che gestisce quanto detto in maniera completamente trasparente. [AGGIORNATO|14 Marzo]
E’ stato corretto un bug che impediva l’aggiunta di più download simultanei. (Download)


Ritornare alla prima pagina di Logo Paperblog