2009-11-13 2 views
37

У меня возникла проблема, когда я пытался выполнять асинхронные запросы на сервер из фонового потока. Я никогда не получал результатов этих запросов. Простой пример, который показывает проблему:Асинхронный запрос к серверу из фонового потока

@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 

Тогда я называю это из основного потока

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

метод downloadImage приведен ниже:

-(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]; 
} 

Проблема заключается в том, что метод imageDownloadDidFinish никогда не вызывается , Кроме того, ни один из методов

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

. Однако, если я заменю

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

по

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

все работает правильно. Я предполагаю, что фоновый поток умирает до запроса async, завершает работу, и это вызывает проблему, но я не уверен. Правильно ли я с этими предположениями? Есть ли способ избежать этой проблемы?

Я знаю, что могу использовать запрос синхронизации, чтобы избежать этой проблемы, но это просто простой пример, реальная ситуация сложнее.

Заранее спасибо.

ответ

59

Да, поток выходит. Вы можете увидеть это, добавив:

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

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

Вы можете держать нить от выхода с:

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

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

Однако это не означает, что поток будет никогда выхода. Поэтому вам нужно остановить его, когда закончите.

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

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

Узнайте больше о Run Loops в Threading Guide и RunLoop Reference.

+0

После исследования в течение многих часов (8+) о том, как выполнить асинхронный NSURLConnection внутри NSInvocationOperation, ни одно из решений не было такой же элегантный, как CFRunLoopRun() и CFRunLoopStop(). Это намного лучше, чем возиться с переменными экземпляра «isExecuting», используя протокол KVO. Следует отметить, что я использовал @selector (выполнитьSelectorOnMainThread: withObject: waitUntilDone: modes :), передавая [NSArray arrayWithObject: NSDefaultRunLoopMode] в качестве режимов. Основной поток для удовлетворения требований UIKit, NSDefaultRunLoopMode, чтобы поддерживать взаимодействие с пользовательским интерфейсом. Большое спасибо! –

+0

Жаль, что я не смог проголосовать больше. Это было очень полезно. –

+6

Для проектов, скомпилированных с помощью ARC, 'NSAutoReleasePool' недоступен, поэтому код будет выглядеть как' @autoreleasepool {[self downloadImage: urlString]; CFRunLoopRun(); } ' – alokoko

0

NSURLRequests полностью асинхронны в любом случае. Если вам нужно сделать NSURLRequest из потока, отличного от основного потока, я думаю, что лучший способ сделать это просто сделать NSURLRequestот основного потока.

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

Все это делает отстреливать запрос HTTP от основного потока (так, что он на самом деле работает и не таинственно исчезают). Ответ HTTP будет возвращаться в обратные вызовы, как обычно.

Если вы хотите сделать это с НОД, вы можете просто пойти

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

The MAIN_QUEUE работает на основном потоке.

Таким образом, первая линия моего HTTP получить функцию выглядит следующим образом:

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

Вы можете начать соединение на фоне потока, но вы должны убедиться, делегат методы называются на главном потоке. Это не может быть сделано с

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

начиная с момента запуска.

ли это настроить очередь делегата, и она работает даже на вторичных потоков:

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