2009-11-13 8 views
37

J'ai le problème lorsque j'ai essayé de faire des requêtes asynchrones au serveur à partir du thread d'arrière-plan. Je n'ai jamais eu les résultats de ces demandes. Exemple simple qui montre le problème:Demande asynchrone au serveur à partir du thread d'arrière-plan

@protocol AsyncImgRequestDelegate 
-(void) imageDownloadDidFinish:(UIImage*) img; 
@end 


@interface AsyncImgRequest : NSObject 
{ 
NSMutableData* receivedData; 
id<AsyncImgRequestDelegate> delegate; 
} 

@property (nonatomic,retain) id<AsyncImgRequestDelegate> delegate; 

-(void) downloadImage:(NSString*) url ; 

@end 



@implementation AsyncImgRequest 
-(void) downloadImage:(NSString*) url 
{ 
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:url] 
      cachePolicy:NSURLRequestUseProtocolCachePolicy 
      timeoutInterval:20.0]; 
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self]; 
if (theConnection) { 
    receivedData=[[NSMutableData data] retain]; 
} else { 
} 

} 

- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
{ 
    [delegate imageDownloadDidFinish:[UIImage imageWithData:receivedData]]; 
    [connection release]; 
    [receivedData release]; 
} 
@end 

J'appelle cela de fil conducteur

asyncImgRequest = [[AsyncImgRequest alloc] init]; 
asyncImgRequest.delegate = self; 
[self performSelectorInBackground:@selector(downloadImage) withObject:nil]; 
downloadImage méthode

est ci-dessous:

-(void) downloadImage 
{ 
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 
[asyncImgRequest downloadImage:@"http://photography.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/Photography/Images/POD/l/leopard-namibia-sw.jpg"]; 
[pool release]; 
} 

Le problème est que imageDownloadDidFinish méthode est jamais appelée . De plus aucune des méthodes

- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse*)response 

sont appelées. Toutefois, si je remplace

[self performSelectorInBackground:@selector(downloadImage) withObject:nil]; 

par

[self performSelector:@selector(downloadImage) withObject:nil]; 

tout fonctionne correctement. Je suppose que le thread d'arrière-plan meurt avant que la requête async soit terminée et que cela cause le problème mais je ne suis pas sûr. Ai-je raison avec ces hypothèses? Est-il possible d'éviter ce problème?

Je sais que je peux utiliser la demande de synchronisation pour éviter ce problème mais c'est juste un exemple simple, la situation réelle est plus complexe.

Merci d'avance.

Répondre

59

Oui, le thread est en cours de fermeture. Vous pouvez voir cela en ajoutant:

-(void)threadDone:(NSNotification*)arg 
{ 
    NSLog(@"Thread exiting"); 
} 

[[NSNotificationCenter defaultCenter] addObserver:self 
             selector:@selector(threadDone:) 
              name:NSThreadWillExitNotification 
              object:nil]; 

Vous pouvez garder le fil de sortir avec:

-(void) downloadImage 
{ 
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 
    [self downloadImage:urlString]; 

    CFRunLoopRun(); // Avoid thread exiting 
    [pool release]; 
} 

Cependant, cela signifie que le fil ne sera jamais sortie. Donc vous devez l'arrêter quand vous avez terminé.

- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
{ 
    CFRunLoopStop(CFRunLoopGetCurrent()); 
} 

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{ 
    CFRunLoopStop(CFRunLoopGetCurrent()); 
} 

En savoir plus sur Run boucles dans le Threading Guide et RunLoop Reference.

+0

Après avoir étudié pendant de nombreuses heures (8+) sur la façon d'effectuer une NSURLConnection asynchrone dans un NSInvocationOperation, aucune des solutions ont été aussi élégant que CFRunLoopRun() et CFRunLoopStop(). C'est BEAUCOUP mieux que de jouer avec les variables d'instance "isExecuting" en utilisant le protocole KVO. Une chose à noter est que j'ai utilisé @selector (performSelectorOnMainThread: withObject: waitUntilDone: modes :) en passant [NSArray arrayWithObject: NSDefaultRunLoopMode] comme modes. Fil principal pour satisfaire les exigences UIKit, NSDefaultRunLoopMode pour maintenir l'interaction de l'interface utilisateur lisse. Merci beaucoup! –

+0

Je voudrais pouvoir voter plus. C'était extrêmement utile. –

+6

Pour les projets compilés avec ARC, NSAutoReleasePool n'est pas disponible, donc le code ressemblerait à '@autoreleasepool {[self downloadImage: urlString]; CFRunLoopRun(); } ' – alokoko

0

NSURLRequettes sont tout de même complètement asynchrones. Si vous avez besoin de faire un NSURLRequest d'un fil autre que le thread principal, je pense que la meilleure façon de le faire est juste faire la NSURLRequestdu thread principal.

// Code running on _not the main thread_: 
[self performSelectorOnMainThread:@selector(SomeSelectorThatMakesNSURLRequest) 
     withObject:nil 
     waitUntilDone:FALSE] ; // DON'T block this thread until the selector completes. 

Tout cela ne fait Rejeton la requête HTTP du thread principal (pour que cela fonctionne et ne fait pas disparaître mystérieusement). La réponse HTTP reviendra dans les rappels comme d'habitude.

Si vous voulez faire avec GCD, vous pouvez simplement aller

// From NOT the main thread: 
dispatch_async(dispatch_get_main_queue(), ^{ // 
    // Perform your HTTP request (this runs on the main thread) 
}) ; 

Les MAIN_QUEUE fonctionne sur le thread principal.

Ainsi, la première ligne de ma fonction HTTP se ressemble:

void Server::get(string queryString, function<void (char*resp, int len) > onSuccess, 
        function<void (char*resp, int len) > onFail) 
{ 
    if(![NSThread isMainThread]) 
    { 
     warning("You are issuing an HTTP request on NOT the main thread. " 
       "This is a problem because if your thread exits too early, " 
       "I will be terminated and my delegates won't run") ; 

     // From NOT the main thread: 
     dispatch_async(dispatch_get_main_queue(), ^{ 
      // Perform your HTTP request (this runs on the main thread) 
      get(queryString, onSuccess, onFail) ; // re-issue the same HTTP request, 
      // but on the main thread. 
     }) ; 

     return ; 
    } 
    // proceed with HTTP request normally 
} 
1

Vous pouvez lancer la connexion sur un thread d'arrière-plan, mais vous devez assurer les méthodes délégués sont appelés thread principal. Cela ne peut pas être fait avec

[[NSURLConnection alloc] initWithRequest:urlRequest 
           delegate:self]; 

puisqu'il commence immédiatement.

Faites ceci pour configurer la file d'attente des délégués et il fonctionne même sur les sujets secondaires:

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:urlRequest 
                   delegate:self 
                 startImmediately:NO]; 
[connection setDelegateQueue:[NSOperationQueue mainQueue]]; 
[connection start]; 
Questions connexes