Асинхронный запрос к серверу из фонового потока
У меня возникла проблема, когда я пытался выполнять асинхронные запросы на сервер из фонового потока. Я никогда не получал результатов этих запросов. Простой пример, который показывает проблему:
@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, завершает работу, и это вызывает проблему, но я не уверен. Правильно ли я с этими предположениями? Есть ли способ избежать этой проблемы?
Я знаю, что могу использовать запрос синхронизации, чтобы избежать этой проблемы, но это просто простой пример, реальная ситуация сложнее.
Спасибо заранее.
Ответы
Ответ 1
Да, поток выходит. Вы можете увидеть это, добавив:
-(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());
}
Подробнее о циклах запуска в Руководство по Threading и Ссылка RunLoop.
Ответ 2
Вы можете запустить соединение в фоновом потоке, но вы должны обеспечить, чтобы методы делегата вызывались в основном потоке. Это невозможно сделать с помощью
[[NSURLConnection alloc] initWithRequest:urlRequest
delegate:self];
так как он начинается немедленно.
Сделайте это, чтобы настроить очередь делегатов, и работает даже на вторичных потоках:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:urlRequest
delegate:self
startImmediately:NO];
[connection setDelegateQueue:[NSOperationQueue mainQueue]];
[connection start];
Ответ 3
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 будет возвращен в обратные вызовы, как обычно.
Если вы хотите сделать это с помощью GCD, вы можете просто пойти
// From NOT the main thread:
dispatch_async( dispatch_get_main_queue(), ^{ //
// Perform your HTTP request (this runs on the main thread)
} ) ;
MAIN_QUEUE
работает в основном потоке.
Итак, первая строка моей функции HTTP get выглядит так:
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
}