[Ruby] NSURLRequestとか

新たな問題

最初にアイテムを追加する時はアイコンをいわゆるスピナーにしておいて、非同期に画像を読みにいって、順次入れかえていくという挙動を実現してみました*1

最初にやったのは、Ruby側でThreadを立てておいて、アイテムを追加する度にQueueを経由でそのスレッドに画像読みこみタスクを渡すという方法です。

これで、カートを読みにいく動作がアイコンの読み込みでブロックされる事もなくなり、スピードアップするだろうと期待したのですが、なんだか返って遅くなったような気が…

測ってみると、ダウンロードがメインスレッドで実行してた時の10倍くらい遅くなってます。

家のネットワークでは、以下のページを最後まで読むのに1.7秒くらい、それから画像のURLを抜きだして画像そのものを読むのに0.4秒くらいかかります。
http://www.amazon.co.jp/gp/aw/d.html?a=4798023809
ところが、別スレッドで動かすとそれが18秒と4秒くらいになってしまったのです。

NSURLRequest

ネットでいろいろ調べると、そもそもRubyCocoaのアプリでTreadを使ってGUIをアップデートするのはお勧めではないらしい。

http://www.unfitforprint.com/articles/2008/02/14/working-with-threads-in-rubycocoa

を見ると"never call anything GUI-related from a separate thread,"とか書いてあるし…
バックグラウンドでダウンロードしたい時はNSURLConnectionがお勧めらしい。

MacRubyチュートリアルに丁度NSURLConnectionを使ったコードがあったので、それを丸ごと真似して以下のようにやってみました。

class DataRequest < OSX::NSObject
  include OSX

  def get(url, &blk)
    @buf = NSMutableData.new
    @blk = blk
    req = NSURLRequest.requestWithURL(NSURL.URLWithString(url))
    NSURLConnection.alloc.initWithRequest_delegate_(req, self)
  end
  
  def connection_didReceiveResponse(conn, resp)
    @buf.setLength(0)
  end
  
  def connection_didReceiveData(conn, data)
    @buf.appendData(data)
  end

  def connection_didFailWithError(conn, err)
    NSLog "Request failed"
  end
  
  def connectionDidFinishLoading(conn)
    @blk.call(@buf)
  end
end

これを以下の様に使います。

  def get(url, &blk)
    DataRequest.new.get(url, &blk)
  end

  def set_icon_async(asin)
    if(self.smallimage)
      self.icon = image(self.smallimage)
    else
      self.icon = SPINNER
      get("http://www.amazon.co.jp/gp/aw/d.html?a=#{asin}") do |data|
        body = NSString.alloc.initWithData_encoding_(data, NSShiftJISStringEncoding)
        if(/<img src="([^\"]+\.jpg)"/ =~ body) 
          get($1) do |data|
            self.smallimage = data.bytes.bytestr(data.length)
            self.icon = NSImage.alloc.initWithData(data)
            self.smallimage = nil if self.icon.nil?      
          end
        end
      end
    end
  end

getメソッド自体はHTTPのリクエストを出すだけだしてすぐ終了します。そしてダウンロードが終ったところで、元のスレッド上で指定ブロックが呼ばれるという動作です。

試してみたところ2秒ほどでダウンロードが終るようになりました。

画像のURLはの割と最初の方に記述されているので、URLを見付けたらそこで読みこみを打ち切って、次に進むようにすればさらに時間を短縮できるかもしれません。

クラッシュする

NSURLConnectionに切り替えてからアプリがクラッシュするようになってしまいました。再現しないのでデバッグするのも難しいです。

どうしたものやら。

*1: ちなみにスピナーの画像は/Applications/iWeb.app/Contents/Resources/Widgets/RSS Feed.iwdgt/Indeterminate-Spinner-Gray-Fast.gifのを使っています。