+[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_async
で NSURLConnection
の何かを呼ぶ 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_queue
は man
によると、次のような引数と戻り値だそうです。
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_async
は man
によると、
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
レジスタなので見てみます。あ、すごい寄り道をしましたが、lldb
は dispatch_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.h
の id
の定義を見ると
typedef struct objc_object {
Class isa;
} *id;
となっているので、まず isa
の Class
を得て 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 まで教えてください!
-
なんでこんな名前かっていうと、New YorkのGrand Central駅から線路がいろんな方向に伸びてるから、ってことらしい。そういえばちゃんとApple Storeができていました。 ↩︎
-
Session 207、Session 208 の Network Apps for iPhone OS Part 1 と 2 を見るべし ↩︎
-
Session 208でちゃんとその事実を述べているんだけど、わりといろんな人がハマってるご様子。バックグラウンドスレッドを作ったはいいけれど、
-[NSURLConnection start]
だけ呼んでNSRunLoop
を回さずに動かないぃぃ!なんていうことはよくある間違いですね。 ↩︎ -
iOS 4 は OTA、いわゆる端末単体でのアップデートが出来ないため、この問題が厳しい。iPhone 4 は iOS 4 で出荷されて売りまくったので携帯の買い替え期間である2年が経過するまではまだまだiOS 4は生き残り続けるだろう。ま、Android はもっとバージョンとハードウェアのフラグメンテーションが酷いので、それに比べたら天国ですが。 ↩︎
-
いつ完了したかわからないので GCD のセマフォを使っています。セマフォは単なる利用可能かどうかを示すカウンタです。リクエストが完了すると
dispatch_semaphore_signal
でカウントアップしてリソースが準備できたことを通知します。dispatch_semaphore_wait
はリソースの準備ができるまで、0じゃなくなるまで待ちます。 ↩︎ -
GCD は簡単です。並列動作させたい仕事をキューに突っ込むとあとはカーネルレベルでうまい具合にスレッドを生成して並列処理してくれるというものです。マルチスレッドがどーのこーのとかあんまり考えなくていいです。あるレベルまでは。 ↩︎
-
やってみるとわかります。
bt
するとぐぬぬ、となります。 ↩︎ -
と、言うわりにはこの記事ではほとんど GDB 互換のショートカットなコマンドしか使ってないので安心です。 ↩︎
-
x86_64 の環境では呼び出しの引数は
rdi
、rsi
、rdx
、rcx
、r8
、r9
のレジスタに格納されます。戻り値はrax
です。 ↩︎ -
sizeof(void*)
は 8、sizeof(int)
は 4 です。memory read -c 24 $rsi
でも眺めてみましょう。 ↩︎