+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:] が何をしているか調べてみた

TL; DR: +[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]dispatch_async+[NSURLConnection sendSynchronousRequest:returningResponse:error:] を呼んでいる。


NSURLConnection を非同期に使いたいなんて要望はもうそれこそかなり昔からあって、数多の車輪の再発明とブログと FAQ が生まれたわけですが、一昨年の Grand Central Dispatch1、GCD の登場でついに抜本的な解決が見えるかのように見えてわひょーいってなった割には2010年の WWDC のネットワークのセッション2で GCD は未来、諸刃の刃、素人にはオススメしないとか言ってたりしていて、おいマジかよとか思ったのは記憶に新しいかと思います。

そんな昨今、あの NSRunLoop を回さないとロクに仕事をしなくなった3ことで有名な NSURLConnection にひっそり sendAsynchronousRequest:queue:completionHandler: なんていうメソッドが生えているではありませんか。

こ、これはっ!もうこれ使えば NSRunLoop 回したりとかいらねえんじゃね?ってなって歓喜なわけですが、よく見るとですね、

Availability

Available in Mac OS X v10.7 and later. Available in iOS 5.0 and later.

って書かれていてガックシなわけですね。Mac OS X はいいとしても、iOS のほうはまだ iOS 4 な iPhone がたっぷりこの世にある以上無視できない4わけで… ぐぬぬと。

じゃあ、っていうんで当然、挙動を調べて互換のコード書いておこうってなります(ならないか… いや、なることにしてください。)

ということで、今回は +[NSURLConnection sendAsynchronousRequest:queue:completionHandler:] が何をしているか調べる過程でlldbの使い方や GCD のことやブロックの実装がどうなっているのかとか Objective-C がどうやって動いているのかを調べてみます。

まずは使ってみる

まず、簡単な短いコードを書いてどうやって動くのか確かめます。 ちょっとハマったのは queue: の部分で、これは completionHandler: を実行するための NSOperationQueue を渡すので、ここで渡す queue に対して -[NSOperationQueue waitUntilAllOperationsAreFinished] をしてもうまく待てなかったりします。で、こんな感じのコードを書きました5

#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>

int main(int argc, char *argv[])
{
    @autoreleasepool {
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        NSURL *url = [NSURL URLWithString:@"http://www.google.com/"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];

        dispatch_semaphore_t s = dispatch_semaphore_create(0);

        [NSURLConnection sendAsynchronousRequest:request
            queue:queue
            completionHandler:^(NSURLResponse *r, NSData *d, NSError *e) {
            NSLog(@"%@", r);
            dispatch_semaphore_signal(s);
        }];

        dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
        dispatch_release(s);
    }
    return 0;
}

何もしていませんね。リクエスト投げて終わり。

clang -g -framework Foundation -o test test.m

とかでコンパイルします。実行すると、まあちゃんと動きますね。

はじめに

こからが本題です。何をしているのか知りたいと。まずは予想します。明らかに GCD は使っていると思われます。

なので、dispatch_asyncNSURLConnection の何かを呼ぶ block を突っ込んでいるだろうと。問題は、どのキューにどんなことをするブロックが突っ込まれているかわかんないということですね6

最終的に呼ばれる completionHandler はまた別の NSOperationQueue (内部的にはGCDになってる) で呼ばれてしまうのでここから実際にリクエストを投げているブロックには到達できないのですね7

というけでやってみます。

デバッガ起動

lldb ./test

今回は lldb を使います。基本的には同じなのですが、コマンド体系が違って困ります8。 そんなときはここに対応表があるので便利です。

Current executable set to './test' (x86_64).
(lldb) 

では、まずおもむろにターゲットのメソッドに b でブレークポイントを立てます。

(lldb) b +[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]

それで、r で走らせて止まるまで行きます。

(lldb) r
...
Process 18965 stopped
* thread #1: tid = 0x1f03, 0x00007fff90c4248e Foundation`+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:], stop reason = breakpoint 1.1
    frame #0: 0x00007fff90c4248e Foundation`+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]
Foundation`+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]:
-> 0x7fff90c4248e:  pushq  %rbp
   0x7fff90c4248f:  movq   %rsp, %rbp
   0x7fff90c42492:  subq   $64, %rsp
   0x7fff90c42496:  movq   1756339(%rip), %rax    ; 0x00007fff90def150
(lldb)

はい、止まりました。

GCD の呼び出しを追う

ここからが勝負です。実際の挙動が予想通りだと期待して、GCD 関係の API にブレークポイントを立てておきます。

(lldb) b dispatch_get_global_queue
(lldb) b dispatch_async

では c で続きを。

Process 18965 stopped
* thread #1: tid = 0x1f03, 0x00007fff893b201d libdispatch.dylib`dispatch_get_global_queue, stop reason = breakpoint 2.1
    frame #0: 0x00007fff893b201d libdispatch.dylib`dispatch_get_global_queue
libdispatch.dylib`dispatch_get_global_queue:
-> 0x7fff893b201d:  testq  $-3, %rsi
   0x7fff893b2024:  jne    0x00007fff893b20b0     ; dispatch_get_global_queue + 147
   0x7fff893b202a:  testb  $2, %sil
   0x7fff893b202e:  je     0x00007fff893b2068     ; dispatch_get_global_queue + 75
(lldb)

おお、dispatch_get_global_queue で止まりました。やっぱり GCD を使ってますね。自分で作ったキューではなくてグローバルのものを使っているようです。 dispatch_get_global_queueman によると、次のような引数と戻り値だそうです。

 dispatch_queue_t
 dispatch_get_global_queue(long priority, unsigned long flags);

つまり、1つめの引数を見ればどのグローバルのキューを使っているのかわかって、戻り値でそのキューの場所がわかります。1つめの引数は rdi レジスタに格納されるので見てみます9

(lldb) p (long)$rdi
(long) $0 = 0

はー、0 ですか。これは dispatch/queue.h によると DISPATCH_QUEUE_PRIORITY_DEFAULT ですね。ではこのキューの場所を押さえておきます。戻り値は rax レジスタに入るので、

(lldb) finish
(lldb) p (void*)$rax
(void *) $1 = 0x00007fff74d6a0c0

ここだそうです。はい覚えた。じゃ c で続きを見ていきます。

dispatch_async の呼び出し

Process 18965 stopped
* thread #1: tid = 0x1f03, 0x00007fff893b3ae9 libdispatch.dylib`dispatch_async, stop reason = breakpoint 3.1
    frame #0: 0x00007fff893b3ae9 libdispatch.dylib`dispatch_async
libdispatch.dylib`dispatch_async:
-> 0x7fff893b3ae9:  pushq  %rbp
   0x7fff893b3aea:  movq   %rsp, %rbp
   0x7fff893b3aed:  pushq  %rbx
   0x7fff893b3aee:  subq   $8, %rsp
(lldb)

今度は、dispatch_async で止まりましたね。どうやら予想通りなにやらブロックをキューに追加しているようですよ。dispatch_asyncman によると、

 void
 dispatch_async(dispatch_queue_t queue, void (^block)(void));

ということなので、最初の引数が追加するキューの場所、2つめがブロックとなります。ではそれぞれ確認しましょう。1つめの引数は rdi ですね。

(lldb) p (void *)$rdi
(void *) $2 = 0x00007fff74d6a0c0

おや、どこかで見た数字。そうです、さっき手に入れた DISPATCH_QUEUE_PRIORITY_DEFAULT なグローバルなキューですね。ここにブロックを追加していると。ふむ、このブロックが多分非同期にネットワークのリクエストを投げていることでしょう。ではこのブロックが呼ばれる箇所でブレークポイントを立てる… となるわけですが、これがちょっと難しい。

ブロックの実体について

ブロックは実体はCの関数ポインタのようなものですが、実はもっと面倒なことをやっています。次のような超絶短いコードを書いて clang -rewrite-objc で C++ のソースを生成させて実際の dispatch_async の呼び出しがブロック無しではどうなるのかを確認します。

#include <dispatch/dispatch.h>
int main() {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{});
}

何もしなさすぎですね。4行。しかし、これがコンパイラの手にかかると凄まじい行数になります。

$ clang -rewrite-objc block.m
$ wc -l block.cpp
      93 block.cpp

うぉ… 100行くらいになっとるではないか。当社比25倍。 うろたえますが、中身を見てみます。どうなってんのかなぁ…

dispatch_async(dispatch_get_global_queue(0, 0),
    (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0,
        &__main_block_desc_0_DATA));

長い、長いぜ… でも、ブロックのところは __main_block_impl_0 構造体のコンストラクタ呼び出しになっています。一瞬あれ?って思いますが、これ C++ のコードです。1つめの引数は __main_block_func_0 で、これがもともとブロックの中身、実体です。で、そのコンストラクタではブロックの実体を impl.FuncPtr に格納しています。

struct __main_block_impl_0 {
  struct __block_impl impl;
  ...
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    ...
    impl.FuncPtr = fp;
    ...
  }
}

impl__block_impl で、中身は

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

となっています。えーっとつまり、2つめの引数に入ってる __main_block_impl_0 構造体の先頭は __block_impl 構造体で、__block_impl 構造体の4つめの FuncPtr がブロックの実体を指しているわけですね。

ブロックの呼び出しを追う

ここまで分かればブレークポイントを立てられます。2つめの引数は rsi レジスタなので見てみます。あ、すごい寄り道をしましたが、lldbdispatch_async を呼び出したところで止まってます。

(lldb) p (void *)$rsi
(void *) $3 = 0x00007fff5fbff6a8

これが先ほどの例でいうところの __main_block_impl_0 構造体の先頭で、つまり、__block_impl 構造体の先頭でもあるので、ここから __block_impl 構造体の最初の3つの size(void *) + size(int) + size(int) 分、つまり16バイトを読み飛ばしてそこがブロックの実体のアドレスになります10

(lldb) p *(void **)($rsi+16)
(void *) $4 = 0x00007fff90c427dd

これだー!ここがブロックの実体だ!ってことでブレークポイントをぽちっとな。

(lldb) b 0x00007fff90c427dd

ふう。これでブロックの中身、たぶんリクエストを非同期で実行する実体が書かれた箇所が呼ばれたら止まります。では c で続きを。

Process 18965 stopped
* thread #6: tid = 0x2703, 0x00007fff90c427dd Foundation`__+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]_block_invoke_1, stop reason = breakpoint 4.1
    frame #0: 0x00007fff90c427dd Foundation`__+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]_block_invoke_1
Foundation`__+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]_block_invoke_1:
-> 0x7fff90c427dd:  pushq  %rbp
   0x7fff90c427de:  movq   %rsp, %rbp
   0x7fff90c427e1:  pushq  %r14
   0x7fff90c427e3:  pushq  %rbx
(lldb)

はい、ちゃんとブロックの中身で止まりました。スレッド番号がこれまでと違いますね。ちゃんと並列化されているようです。って、ちょっ、ちゃんとブロックの実体の名前が表示されてるじゃん! そうです、実は

(lldb) b __+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]_block_invoke_1

ってやればブロックで止められたんです。まあそんなことわからなかったのでブロックの勉強になったってことでよしとします。

Objetive-Cのコードを追う

ここからはObjective-Cな感じで。Objective-Cのメソッド呼び出しはすべてランタイムの objc_msgSend で行われます。中身はアセンブラらしいですが11、Objective-Cはコンパイラとランタイムによる協奏曲のようなもので、Cで動的で美しいですね。 ということで objc_msgSend の呼び出しを監視すれば、Objective-Cのコードがなにをしているのか大体わかります。ではここにブレイクポイントを立てておきます。

(lldb) b objc_msgSend

では c で続けます。

Process 18965 stopped
* thread #6: tid = 0x2703, 0x00007fff8d9a8e80 libobjc.A.dylib`objc_msgSend, stop reason = breakpoint 5.1
    frame #0: 0x00007fff8d9a8e80 libobjc.A.dylib`objc_msgSend
libobjc.A.dylib`objc_msgSend:
-> 0x7fff8d9a8e80:  testq  %rdi, %rdi
   0x7fff8d9a8e83:  je     0x00007fff8d9a8eb0     ; objc_msgSend + 48
   0x7fff8d9a8e85:  testb  $1, %dil
   0x7fff8d9a8e89:  jne    0x00007fff8d9a8ec7     ; objc_msgSend + 71
(lldb)

はい、止まりました。objc_msgSend はリファレンスによると、

id objc_msgSend(id theReceiver, SEL theSelector, ...)

ということなので、一つめの引数がメソッドを呼び出す対象のオブジェクトで二つめがメソッド名となります。SEL の名前は sel_getName で見られます。id からクラス名を得るのはちょっとめんどくさくて、objc/objc.hid の定義を見ると

typedef struct objc_object {
     Class isa;
} *id;

となっているので、まず isaClass を得て class_getName を使います。ということでこうなります。

(lldb) p (const char *)class_getName(((id)$rdi)->isa)
(const char *) $5 = 0x00007fff90d22f23 "NSURLConnection"
(lldb) p (const char *)sel_getName($rsi)
(const char *) $6 = 0x00007fff8b1f75f6 "sendSynchronousRequest:returningResponse:error:"

結論

なんと!そういうことか!なんと、ブロックの中で +[NSURLConnection sendSynchronousRequest:returningResponse:error:] を呼び出しているではありませんか。 どうやらこれが答えのようです。

+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:] は GCD で同期呼び出しである +[NSURLConnection sendSynchronousRequest:returningResponse:error:] をつかっていました!NSRunLoop なんて回していないし、NSThread でゴチャゴチャやっていない。いやはや。

というわけで、すでにすごい数ある NSURLConnection のFAQにもうひとつ追加。

NSURLConnection を非同期に簡単に並列化したい場合は、GCD の dispatch_async+[NSURLConnection sendSynchronousRequest:returningResponse:error:] を呼び出す。

最初につくったものと同じような動きをするコードではこうなります。

#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>

int main(int argc, char *argv[])
{
    @autoreleasepool {
        dispatch_semaphore_t s = dispatch_semaphore_create(0);

        dispatch_queue_t queue = dispatch_get_global_queue(
                                 DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(queue, ^{
            NSURL *url = [NSURL URLWithString:@"http://www.google.com/"];
            NSURLRequest *request = [NSURLRequest requestWithURL:url];
            NSURLResponse *response;
            NSError *error;

            [NSURLConnection sendSynchronousRequest:request
                                  returningResponse:&response
                                              error:&error];

            NSLog(@"%@", response);

            dispatch_semaphore_signal(s);
        });
        dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
        dispatch_release(s);
    }
    return 0;
}

まあ、いろんな状況があるのでこれが最適解かといえばそうでもないと思います。 これだと途中でリクエストを止められないとか問題がある気がしますし、その場合は、いまのところは NSRunLoop を回してバックグラウンドスレッドでぐんにょりみたいな感じでやることになるんでしょうね。

以上、長くなりました。

もっとこうするとかっこ良く挙動が調べられるよ!とか @niw まで教えてください!

  1. なんでこんな名前かっていうと、New YorkのGrand Central駅から線路がいろんな方向に伸びてるから、ってことらしい。そういえばちゃんとApple Storeができていました。 ↩︎

  2. Session 207Session 208 の Network Apps for iPhone OS Part 1 と 2 を見るべし ↩︎

  3. Session 208でちゃんとその事実を述べているんだけど、わりといろんな人がハマってるご様子。バックグラウンドスレッドを作ったはいいけれど、-[NSURLConnection start] だけ呼んで NSRunLoop を回さずに動かないぃぃ!なんていうことはよくある間違いですね。 ↩︎

  4. iOS 4 は OTA、いわゆる端末単体でのアップデートが出来ないため、この問題が厳しい。iPhone 4 は iOS 4 で出荷されて売りまくったので携帯の買い替え期間である2年が経過するまではまだまだiOS 4は生き残り続けるだろう。ま、Android はもっとバージョンとハードウェアのフラグメンテーションが酷いので、それに比べたら天国ですが。 ↩︎

  5. いつ完了したかわからないので GCD のセマフォを使っています。セマフォは単なる利用可能かどうかを示すカウンタです。リクエストが完了すると dispatch_semaphore_signal でカウントアップしてリソースが準備できたことを通知します。dispatch_semaphore_wait はリソースの準備ができるまで、0じゃなくなるまで待ちます。 ↩︎

  6. GCD は簡単です。並列動作させたい仕事をキューに突っ込むとあとはカーネルレベルでうまい具合にスレッドを生成して並列処理してくれるというものです。マルチスレッドがどーのこーのとかあんまり考えなくていいです。あるレベルまでは。 ↩︎

  7. やってみるとわかります。bt するとぐぬぬ、となります。 ↩︎

  8. と、言うわりにはこの記事ではほとんど GDB 互換のショートカットなコマンドしか使ってないので安心です。 ↩︎

  9. x86_64 の環境では呼び出しの引数は rdirsirdxrcxr8r9 のレジスタに格納されます。戻り値は rax です。 ↩︎

  10. sizeof(void*) は 8、sizeof(int) は 4 です。memory read -c 24 $rsiでも眺めてみましょう。 ↩︎

  11. これです。 ↩︎