2012/12/18

バックグランドで非同期通信する際のハマり所 in RubyMotion

この記事は RubyMotion Advent Calendar 2012 の18日目の記事です。

週末にアプリをシコシコとゴリゴリと書いていたのですけども。ハマったわー。いやーハマったわー。

 

やりたかったこと

以下の処理を、3以降は別スレッドで実行というのがやりたかったことです。

  1. WebView でコンテンツをロード
  2. WebViewのコンテンツのロードが完了したタイミングでコンテンツ内の文字列を改行で split
  3. splitした各文字列を某APIにHTTPリクエスト
  4. APIのレスポンスをファイルに保存したり配列に格納したり

 

ソースイメージ

ハマりどころ解説のためのソースイメージです。

 

ポイント概要

ここから本題です。

なんとなく上から目線な表現になっていますが、日本語が不自由なだけです。恐縮です。誤り等ありましたらコメ欄かTwitter ( @amazedkoumei ) でご指摘いただけると嬉しいです。

ハマりどころをまとめると以下の3点です。

  • UIWebView, NSURLConnection, Dispatch::Queueなどはきちんと閉じましょう。
  • 1回のWebページロードについて、UIWebViewDelegate#webViewDidFinishLoadが複数回呼ばれる場合があるので気をつけましょう。
  • NSURLConnection を複数回投げる処理をgcdしたい場合は、delegateメソッド内にDispatch::Queueを書きましょう。

以下、各要点の補足説明になります。

 

ポイントその1:  UIWebView, NSURLConnection, Dispatch::Queueなどはきちんと閉じましょう。

例えば viewDidLoad でUIWebViewをnewした直後にUIWebViewのdelegate先(例えばUIWebViewを作成したViewController)を解放した場合、ページのロード完了時に呼ばれる delegateメソッド "webViewDidFinishLoad" が呼ばれたタイミングでクラッシュします。

ですので、ロード処理自体をストップさせてから安らかに解放される必要があるわけですが、上にあげたUIWebView, NSURLConnection, Dispatch::Queue にはそれぞれ中断メソッドが用意されているのでそれらを呼んであげましょう。メソッド名の統一感はゼロ。

  • UIWebView#stopLoading()
  • NSURLConnection#cancel()
  • Dispatch::Queue#suspend!

追記: 12/12/18
@satococoa にUIWebViewの場合、解放前にdelegateにnilを設定すればいいということをご指摘いただきました。

 

ソースイメージでは sample_view_controller.rb の viewDidDisappear でこの処理をしています。

NSURLConnection をWrapしている BW::HTTP モジュールを使う場合は get | post | put | delete 各メソッドが返す BW::HTTP::Queryオブジェクト内にNSURLConnectionオブジェクトが入っているので、それを大切に大切に補完しておきましょう。(ソースイメージの sample_api.rb の fetch メソッド参照)

BW::HTTP モジュールってなんじゃらホイという方はこちらが分かりやすいです。

RubyMotion - naoyaのはてなダイアリー

 

ポイントその2:  1回のWebページロードについて、UIWebViewDelegate#webViewDidFinishLoadが複数回呼ばれる場合があるので気をつけましょう。

把握できてるのは『ロードしたコンテンツ内にiframeがある場合』です。他にもあるかもしれません。

webViewDidFinishLoad で重めの処理をする場合には気をつけてください。ソースイメージの程度であればなんら問題ないのですが、上述したシコシコゴリゴリのアプリではクラッシュしーのおかしな結果が返りーのともう散々で・・。

その散々だったアプリのソースを公開できてればよかったんですけど、現在『動くけど汚いソース』となっておりましてしばしお時間いただきたく。

 

ポイントその3:  NSURLConnection を複数回投げる処理をgcdしたい場合は、delegateメソッド内にDispatch::Queueを書きましょう。

NSURLConnection.connectionWithRequest(request, delegate:delegate) をDipatch::Queue内でコールしても戻ってきてくれません。いったいどこに行ってしまわれるのか!

ソースイメージでは sample_api.rb の fetch メソッドに該当の記述になります。先述の通り、BW::HTTP モジュールは NSURLConnection をWrapしております。

ちなみに『非メインスレッドで Dipatch::Queue#async を繰り返しコールしていくとどこかで処理が止まってしまう』という問題にも遭遇しまして『 Dipatch::Queue#async はメインスレッドからのみコールする』という対処で乗り切ったのですが、ソースイメージのようなあっさりとした処理だと再現できませんでした。Dipatch::Queue チェーンがどこかで勝手に解放されるせいかな、とは思っておるのですが。

 

最後に

週末の割りと長めの時間を闘いに費やした気がするのですが、まとめるとこんなものか、という。

簡単なソースイメージだけではあれなので、近い将来シコシコゴリゴリアプリをGithubにあげてリンク貼りたいですね。実行可能性としては『明日から本気だす』と同程度とですけども。

こちらからは以上ですw

Comments