NSURLConnectionをサブスレッドで使用する

iOS7でNSURLSessionがでて、少し陰の薄くなったNSURLConnectionですが、まだまだネットワーク処理の中心としてよく使われていますよね。 NSURLConnectionの使い方は比較的簡単ですが、サブスレッドからよぶときには少し注意が必要なのでまとめてみました。

サブスレッド上の処理で何を注意すべきなのか

まず、最初にサブスレッドでの処理で何を一番注意すべきなのかをおさらいしましょう。

iOSでは、画面描画には主にUIKitフレームワークを使用しますが、このフレームワークはメインスレッド上の使用のみが保証されています。 例えばサブスレッド上で画面に表示した画像(UIImageView)を変更したり、ユーザーにダイアログを表示したり(UIAlertView)などの処理を行った場合、表示が遅れてしまったり、表示されなかったり、最悪の場合にはクラッシュする場合もあります。

自分の書いたソースコードでは気をつけても、システムAPIの中にもユーザーへの確認ダイアログを表示するAPIがあるので、注意が必要です。 (Sub Threadでダイアログを表示するようなシステムAPIをよんで、予想外のタイミングでユーザー確認ダイアログが表示されてしまう場合があります。)

NSURLConnectionでネットワークからの返り値が戻ってきたあとに画面描画の処理を行う場合には、必ずメインスレッドで処理をする必要があります。

sendSynchronousRequestによる同期呼び出し

まず、一番単純な sendSynchronousRequest を使った同期呼び出し。 同期呼び出しのため、ネットワークからの処理がかえってくるまでメインスレッドの処理が阻害されてしまい、全くおすすめされる方法ではありません。(もちろんAppleも非推奨です) ただ、処理の前も、ネットワークからの返り値がかえったあともメインスレッドで処理されるので、その点は楽ですね。

NSURL* myURL = [NSURL URLWithString:@"http://www.apple.com/"];
NSURLRequest* myRequest = [NSURLRequest requestWithURL:myURL];

NSURLConnection* myConnection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self startImmediately:NO];

    NSError* error;
    NSURLResponse* res;
    NSData* synchData = [NSURLConnection sendSynchronousRequest:myRequest returningResponse:&res error:&error];
    NSLog(@"NSURLResponse is %@",res);

sendAsynchronousRequestによる非同期よびだし

次は、sendAsynchronousRequestを使った非同期よびだし。 こちらはBlockを使って非同期処理にすることができるので、比較的見通しがいい方法で非同期処理をかくことができます。

ネットワークからの返り値が帰ったときの処理は、メインスレッドの場合とサブスレッドの場合があります。

まず、sendAsynchronousRequestに与えるNSOperationQueueとして、[NSOperationQueue mainQueue]を指定した場合。 この場合には返り値がかえったときのBlockはメインスレッドで実行されます。

NSURL* myURL = [NSURL URLWithString:@"http://www.apple.com/"];
NSURLRequest* myRequest = [NSURLRequest requestWithURL:myURL];

NSURLConnection* myConnection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self startImmediately:NO];

    [NSURLConnection sendAsynchronousRequest:myRequest
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *resp, NSData *data, NSError *error) {
        
                               NSLog(@"NSURLResponse is %@",resp);
        //ここは、メインスレッドで実行されます。
        
                           }];

まず、sendAsynchronousRequestに与えるNSOperationQueueとして、新しく作成したNSOperationQueueを指定した場合。 この場合には返り値がかえったときのBlockはサブスレッドで実行されるので、そのままUIの描画処理を行うのはNGです。

NSURL* myURL = [NSURL URLWithString:@"http://www.apple.com/"];
NSURLRequest* myRequest = [NSURLRequest requestWithURL:myURL];

NSURLConnection* myConnection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self startImmediately:NO];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:myRequest
                                       queue:queue
                           completionHandler:^(NSURLResponse *resp, NSData *data, NSError *error) {
        
                               NSLog(@"NSURLResponse is %@",resp);
        //ここは、NSOperationQueueのサブスレッドで実行されます。
        
                           }];
                                   queue:[NSOperationQueue mainQueue]

メインスレッドからのDelegateを使ったよびだし

さて、次にNSConnectionのDelegateを使った呼び出しも見てみましょう。

NSURLConnectionを作ってstartをよぶだけなので簡単ですよね。

 NSURL* myURL = [NSURL URLWithString:@"http://www.apple.com/"];
    NSURLRequest* myRequest = [NSURLRequest requestWithURL:myURL];
    NSURLConnection* myConnection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self startImmediately:NO];

    [myConnection start];

この場合、ネットーワーク処理が終わったときに下記のDelegate関数がよばれますが、これらはすべてメインスレッド上で実行されます。

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@"didReceiveResponse");
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    NSLog(@"didReceiveData");
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection{   
    NSLog(@"connectionDidFinishLoading");
}

サブスレッドからのDelegateを使ったよびだし(メインスレッドのRunLoopを利用)

さて、サブスレッドでNSURLConnectionを処理したらどうなるでしょうか。 メインスレッド上の処理と同様にNSURLConnectionを作って、サブスレッドのなかからstartしてみます。 (ここではdispatch_get_global_queueの中からよんでいます)

この場合、サブスレッドの中で[myConnection start]をよぶだけでは処理は実行されません。

サブスレッドのNSRunLoopがデフォルトでは動いておらず、そのためにDelegate処理が実行されません。 ここでは、[NSRunLoop mainRunLoop]で、メインスレッドのNSRunLoopにNSURLConnectionを指定して実行しています。

didReceiveResponse などのDelegate関数はすべてメインスレッド上で実行されます。

 NSURL* myURL = [NSURL URLWithString:@"http://www.apple.com/"];
    NSURLRequest* myRequest = [NSURLRequest requestWithURL:myURL];
    NSURLConnection* myConnection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self startImmediately:NO];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{        
        
        if (myConnection != nil) {
            [myConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
            [myConnection start];
        }       
    });

サブスレッドからのDelegateを使ったよびだし(サブスレッドのRunLoopを利用)

次は、メインスレッドのNSRunLoopではなく、サブスレッドのNSRunLoopでNSURLConnectionを処理してみましょう。

[NSRunLoop currentRunLoop]で、サブスレッドのNSRunLoopを取得し、それを遠い未来([NSDate distantFuture])までrunします。

この場合、NSRunLoopのrunUntilDateをする前に、[myConnection start]を呼ばないと正しく実行されません。 (runされたNSRunLoopがすぐ終わってしまいます)

また、この場合Delegate関数はすべてサブスレッド上で実行されます。

 NSURL* myURL = [NSURL URLWithString:@"http://www.apple.com/"];
    NSURLRequest* myRequest = [NSURLRequest requestWithURL:myURL];
    NSURLConnection* myConnection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self startImmediately:NO];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        if (myConnection != nil) {
            [myConnection start];//これで、MyConnectionがRunLoopにattachされる
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
        }
        
        
    });

Run Loopとは何か

そもそもRun Loopとはなんでしょう。

基本的に、プログラムは書いた順番に処理されていき、最後までいくと終了します。

例えばユーザーからのタッチイベントを処理したいときや、非同期にネットワーク処理を行いたいとき、画面のコントロールに移動アニメーションをかけたいときには、定期的にあるコマンドを実行して、それぞれ「ユーザーが画面をタッチしていないか」「ネットワークから処理が戻ってきてないか」「画面のコントロールをちょっとずらして書き換える」などの処理を行わなくてはいけないわけです。 この「定期的にあるコマンドを実行する」しくみがRunLoopとなり、RunLoopはそれぞれのスレッドごとに存在します。

そして、メインスレッドでは自動的にRunLoopが動いていますが、サブスレッドのNSRunLoopはデフォルトでは動いていません。 それで、サブスレッド上で処理をすると、UIの描画処理がうまくいかないし、Delegate処理も動作しないというわけなんです。

サブスレッドでDelegateやTimerの処理を行うときには、RunLoopをrunするか、メインスレッドのRunLoopを使うかどちらかを行う必要があります。

まとめ

  • UIKitを使った画面描画処理は必ずメインスレッドで行う。(システムAPIのなかには、ダイアログを表示するものもあるので気をつける)
  • Delegateの受け処理は、起動したスレッドから行われる。サブスレッドでDelegateの処理やTimerの処理を行う場合には、RunLoopをまわす必要がある。
  • 迷ったら、sendAsynchronousRequestが一番楽です。Queueの指定は[NSOperationQueue mainQueue]に。