iPhoneアプリでNSURLConnectionリクエストがタイムアウトするという断続的な問題が発生しています。最近発生しているようです。この状態になると、その状態のままになります。唯一の解決策は、アプリを強制終了して再起動することのようです。
観察:
- NSURLConnectionを実行するコアコードは変更されていません(最近追加されたカスタムユーザーエージェントコードを除く)。
- 再現可能なケースはまだ見つかりませんが、特に3G(WiFiなし)で実行している場合は、アプリがしばらくバックグラウンドに置かれた後にタイムアウトが発生するようです。
- サーバー上のApacheは、これらのタイムアウトが発生している間、クライアントからの要求をログに記録していません。
- 一貫性はありませんが、MailやSafariなどの他のアプリが影響を受けている(つまり、タイムアウトが発生している)ことを示すいくつかの兆候。
- 3Gのカバレッジは、問題を引き起こしている一時的な問題を除外するためではなく、私がいるところではしっかりしています(可能性は低いと想定されます)。
- すべてのリクエストは独自のAPIサーバーに送信され、残りのPOSTリクエストです。
- timeoutIntervalおよびPOST要求の問題のため、独自のNSTimerベースのタイムアウトを使用します。タイムアウト値を増やして試してみましたが、問題は引き続き発生します。
その他の雑多なもの:
- アプリは最近ARCに変換されました。
- iOS5.1.1でアプリを実行しています。
- アプリはUrbanAirship、TestFlight、FlurrySDKの最新バージョンを使用しています。
- また、TouchXMLのARCブランチを使用して応答を解析します。
以下に示すように、コードはメインスレッドで実行されます。そのスレッドで何かがブロックされていると思いましたが、アプリを一時停止したときに表示されるスタックトレースは、メインスレッドが正常であることを示しています。NSURLConnectionは独自のスレッドを使用しており、ブロックする必要があると思います。
#define relnil(v) (v = nil)
- (id) initWebRequestController
{
self = [super init];
if (self)
{
//setup a queue to execute all web requests on synchronously
dispatch_queue_t aQueue = dispatch_queue_create("com.myapp.webqueue", NULL);
[self setWebQueue:aQueue];
}
return self;
}
- (void) getStuffFromServer
{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
dispatch_sync([self webQueue], ^{
error_block_t errorBlock = ^(MyAppAPIStatusCode code, NSError * error){
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didEncounterErrorGettingPointsWithCode:code andOptionalError:error];
});
};
parsing_block_t parsingBlock = ^(CXMLDocument * doc, error_block_t errorHandler){
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:@"apiResult/data/stuff" error:&error];
if (error || !node) {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
else {
stuffString = [node stringValue];
}
if (stuffString) {
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didFinishGettingStuff:stuffString];
});
}
else {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
};
NSURL * url = [[NSURL alloc] initWithString:[NSString stringWithFormat:MyAppURLFormat_MyAppAPI, @"stuff/getStuff"]];
NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:url];
NSMutableDictionary * postDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[[NSUserDefaults standardUserDefaults] objectForKey:MyAppKey_Token], @"token",
origin, @"from",
destination, @"to",
transitTypeString, @"mode",
time, @"time",
nil];
NSString * postString = [WebRequestController httpBodyFromDictionary:postDictionary];
[urlRequest setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
[urlRequest setHTTPMethod:@"POST"];
if (urlRequest)
{
[self performAPIRequest:urlRequest withRequestParameters:postDictionary parsing:parsingBlock errorHandling:errorBlock timeout:kTimeout_Standard];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
relnil(url);
relnil(urlRequest);
});
});
}
- (void) performAPIRequest: (NSMutableURLRequest *) request
withRequestParameters: (NSMutableDictionary *) requestParameters
parsing: (parsing_block_t) parsingBlock
errorHandling: (error_block_t) errorBlock
timeout: (NSTimeInterval) timeout
{
NSAssert([self apiConnection] == nil, @"Requesting before previous request has completed");
NSString * postString = [WebRequestController httpBodyFromDictionary:requestParameters];
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSString * erVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
NSString * erBuildVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
if ([erBuildVersion isEqualToString:erVersion] || [erBuildVersion isEqualToString:@""]) {
erBuildVersion = @"";
} else {
erBuildVersion = [NSString stringWithFormat:@"(%@)", erBuildVersion];
}
NSString * iosVersion = [[UIDevice currentDevice] systemVersion];
NSString * userAgent = [NSString stringWithFormat:@"MyApp/%@%@ iOS/%@", erVersion, erBuildVersion, iosVersion];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
[request setTimeoutInterval:(timeout-3.0f)];
dispatch_sync(dispatch_get_main_queue(), ^{
NSURLConnection * urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if (urlConnection)
{
[self setApiConnection:urlConnection];
requestParseBlock = [parsingBlock copy];
requestErrorBlock = [errorBlock copy];
NSMutableData * aMutableData = [[NSMutableData alloc] init];
[self setReceivedData:aMutableData];
relnil(aMutableData);
[urlConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[urlConnection start];
relnil(urlConnection);
NSTimer * aTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(timeoutTimerFired:) userInfo:nil repeats:NO];
[self setTimeoutTimer:aTimer];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
});
//we want the web requests to appear synchronous from outside of this interface
while ([self apiConnection] != nil)
{
[NSThread sleepForTimeInterval:.25];
}
}
- (void) timeoutTimerFired: (NSTimer *) timer
{
[[self apiConnection] cancel];
relnil(apiConnection);
relnil(receivedData);
[self requestErrorBlock](MyAppAPIStatusCodeTimeout, nil);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self requestErrorBlock](MyAppAPIStatusCodeFailedToConnect, error);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
MyAppAPIStatusCode status = MyAppAPIStatusCodeFailedToParse;
CXMLDocument *doc = [[self receivedData] length] ? [[CXMLDocument alloc] initWithData:[self receivedData] options:0 error:nil] : nil;
DLog(@"response:\n%@", doc);
if (doc)
{
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:@"apiResult/result" error:&error];
if (!error && node)
{
status = [[node stringValue] intValue];
if (status == MyAppAPIStatusCodeOK)
{
[self requestParseBlock](doc, [self requestErrorBlock]);
}
else if (status == MyAppAPIStatusCodeTokenMissingInvalidOrExpired)
{
[Definitions setToken:nil];
[self requestMyAppTokenIfNotPresent];
[Definitions logout];
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestControllerDidRecivedExpiredTokenError:self];
});
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
status = MyAppAPIStatusCodeUnexpectedResponse;
[self requestErrorBlock](status, nil);
}
relnil(doc);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
以下のURLは、アプリが問題のある状態にあったときのキュー/スレッドのスクリーンショットです。ミューテックスの待機は不思議ですが、スレッド10は前のタイムアウトで実行されたキャンセルに関連していると思います。また、スレッド22のFlurryに関するビットは、他の機会に問題が発生したときに一貫して表示されません。
スタックトレースのスクリーンショット:
http://img27.imageshack.us/img27/5614/screenshot20120529at236.png http://img198.imageshack.us/img198/5614/screenshot20120529at236.png
私はiOS/Appleの開発に比較的慣れていないので、おそらくこれらのトレースで明らかに間違っていることを見落としています。
NSURLConnectionと関連コードのソースがあれば、これらすべてを解決するのははるかに簡単ですが、そのように、私はこの時点で暗闇の中で刺し傷を負っています。