+[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。
何もしていませんね。リクエスト投げて終わり。
とかでコンパイルします。実行すると、まあちゃんと動きますね。
はじめに
こからが本題です。何をしているのか知りたいと。まずは予想します。明らかに GCD は使っていると思われます。
なので、dispatch_async
で NSURLConnection
の何かを呼ぶ block
を突っ込んでいるだろうと。問題は、どのキューにどんなことをするブロックが突っ込まれているかわかんないということですね6。
最終的に呼ばれる completionHandler
はまた別の NSOperationQueue
(内部的にはGCDになってる) で呼ばれてしまうのでここから実際にリクエストを投げているブロックには到達できないのですね7。
というけでやってみます。
デバッガ起動
今回は lldb
を使います。基本的には同じなのですが、コマンド体系が違って困ります8。
そんなときはここに対応表があるので便利です。
では、まずおもむろにターゲットのメソッドに b
でブレークポイントを立てます。
それで、r
で走らせて止まるまで行きます。
はい、止まりました。
GCD の呼び出しを追う
ここからが勝負です。実際の挙動が予想通りだと期待して、GCD 関係の API にブレークポイントを立てておきます。
では c
で続きを。
おお、dispatch_get_global_queue
で止まりました。やっぱり GCD を使ってますね。自分で作ったキューではなくてグローバルのものを使っているようです。
dispatch_get_global_queue
は man
によると、次のような引数と戻り値だそうです。
つまり、1つめの引数を見ればどのグローバルのキューを使っているのかわかって、戻り値でそのキューの場所がわかります。1つめの引数は rdi
レジスタに格納されるので見てみます9。
はー、0 ですか。これは dispatch/queue.h
によると DISPATCH_QUEUE_PRIORITY_DEFAULT
ですね。ではこのキューの場所を押さえておきます。戻り値は rax
レジスタに入るので、
ここだそうです。はい覚えた。じゃ c
で続きを見ていきます。
dispatch_async の呼び出し
今度は、dispatch_async
で止まりましたね。どうやら予想通りなにやらブロックをキューに追加しているようですよ。dispatch_async
は man
によると、
ということなので、最初の引数が追加するキューの場所、2つめがブロックとなります。ではそれぞれ確認しましょう。1つめの引数は rdi
ですね。
おや、どこかで見た数字。そうです、さっき手に入れた DISPATCH_QUEUE_PRIORITY_DEFAULT
なグローバルなキューですね。ここにブロックを追加していると。ふむ、このブロックが多分非同期にネットワークのリクエストを投げていることでしょう。ではこのブロックが呼ばれる箇所でブレークポイントを立てる… となるわけですが、これがちょっと難しい。
ブロックの実体について
ブロックは実体はCの関数ポインタのようなものですが、実はもっと面倒なことをやっています。次のような超絶短いコードを書いて clang -rewrite-objc
で C++ のソースを生成させて実際の dispatch_async
の呼び出しがブロック無しではどうなるのかを確認します。
何もしなさすぎですね。4行。しかし、これがコンパイラの手にかかると凄まじい行数になります。
うぉ… 100行くらいになっとるではないか。当社比25倍。 うろたえますが、中身を見てみます。どうなってんのかなぁ…
長い、長いぜ… でも、ブロックのところは __main_block_impl_0
構造体のコンストラクタ呼び出しになっています。一瞬あれ?って思いますが、これ C++ のコードです。1つめの引数は __main_block_func_0
で、これがもともとブロックの中身、実体です。で、そのコンストラクタではブロックの実体を impl.FuncPtr
に格納しています。
impl
は __block_impl
で、中身は
となっています。えーっとつまり、2つめの引数に入ってる __main_block_impl_0
構造体の先頭は __block_impl
構造体で、__block_impl
構造体の4つめの FuncPtr
がブロックの実体を指しているわけですね。
ブロックの呼び出しを追う
ここまで分かればブレークポイントを立てられます。2つめの引数は rsi
レジスタなので見てみます。あ、すごい寄り道をしましたが、lldb
は dispatch_async
を呼び出したところで止まってます。
これが先ほどの例でいうところの __main_block_impl_0
構造体の先頭で、つまり、__block_impl
構造体の先頭でもあるので、ここから __block_impl
構造体の最初の3つの size(void *) + size(int) + size(int)
分、つまり16バイトを読み飛ばしてそこがブロックの実体のアドレスになります10。
これだー!ここがブロックの実体だ!ってことでブレークポイントをぽちっとな。
ふう。これでブロックの中身、たぶんリクエストを非同期で実行する実体が書かれた箇所が呼ばれたら止まります。では c
で続きを。
はい、ちゃんとブロックの中身で止まりました。スレッド番号がこれまでと違いますね。ちゃんと並列化されているようです。って、ちょっ、ちゃんとブロックの実体の名前が表示されてるじゃん! そうです、実は
ってやればブロックで止められたんです。まあそんなことわからなかったのでブロックの勉強になったってことでよしとします。
Objetive-Cのコードを追う
ここからはObjective-Cな感じで。Objective-Cのメソッド呼び出しはすべてランタイムの objc_msgSend
で行われます。中身はアセンブラらしいですが11、Objective-Cはコンパイラとランタイムによる協奏曲のようなもので、Cで動的で美しいですね。
ということで objc_msgSend
の呼び出しを監視すれば、Objective-Cのコードがなにをしているのか大体わかります。ではここにブレイクポイントを立てておきます。
では c
で続けます。
はい、止まりました。objc_msgSend
はリファレンスによると、
ということなので、一つめの引数がメソッドを呼び出す対象のオブジェクトで二つめがメソッド名となります。SEL
の名前は sel_getName
で見られます。id
からクラス名を得るのはちょっとめんどくさくて、objc/objc.h
の id
の定義を見ると
となっているので、まず isa
の Class
を得て class_getName
を使います。ということでこうなります。
結論
なんと!そういうことか!なんと、ブロックの中で +[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:]
を呼び出す。
最初につくったものと同じような動きをするコードではこうなります。
まあ、いろんな状況があるのでこれが最適解かといえばそうでもないと思います。
これだと途中でリクエストを止められないとか問題がある気がしますし、その場合は、いまのところは 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
でも眺めてみましょう。 ↩︎