iphone-rubycocoaで、実機動作中のアプリをリモートから動的に変更する
先日からの iphone-rubycocoa で一番やりたかったこと、それは、リモートからiPhone実機で動いているアプリを、動的に変更することです。たとえば、画面の一部分のちょっとした色とかの値をいじろうと思っても、ObjC のコードに修正を加えて再コンパイルして実機転送して動作させる、というような時間がかかる (下手すると10秒20秒平気でかかる) ことしないとなりません。例えばそれが、インタラクティブなおかつリアルタイムに変更できたらすばらしと思いません? ということでできるようにしてみました。
この@kuさんの記事によると、同様のことをJSCocoaでもできるようです。が、この時はJSCocoaは、シミュレータでしか動かず実機動作はできませんでした。(今はlibffiがちょっと動いている? そうなのでもしかすると、実機動作するかもしれません)
サンプルを動かすには
最新はここですが、順次更新していっていますので
http://github.com/takuma104/iphone-rubycocoa/tree/master
以下のサンプルを動かしているのはこちらのtagのものです。
http://github.com/takuma104/iphone-rubycocoa/commits/hateda-2009-0302
普通に git clone して、そのあと git co hateda-2009-0302 とかすると良いかもです。そのあとXcodeでbuildとかします。
まず、main.rbの
# require 'remote_irb' # RemoteIRB.new(6000).start
の部分のコメントを外します。そうすると、iPhoneなどの実機で、TCPの6000 ポートで受けつけます。
注意:とくにiPhoneだと、3Gが使用可能になっていると、globalが空いてたりして、どこからでもアクセスできてしまったりするので、使うときにはセキュリティに十分注意してください。
あとは普通にビルドして実機あるいはシミュレータで動かしてください。
実機上で起動したのを確認したら、telnetで
telnet 192.168.4.138 6000
とかすると接続してirbのプロンプトが出ます。*1 (アドレスはiPhoneのアドレスに置き代えてくださいね)。ちなみに readline をサポートしてないので、rlwrap というコマンドを使い (sudo port install rlwrapとかでインストールできます)
rlwrap telnet 192.168.4.138 6000
とかすると履歴とか使えて幸せです。
いろいろ触ってみるサンプル
スクリーンショットで説明すると、
から
にするサンプルです。こんなかんじで色々できます。(適宜コメントを入れました)
# とりあえず動くか確認 irb(main):001:0> 1+2 3 # まずMyAppDeletateのインスタンスを得ようと思う irb(main):002:0> OSX::UIApplication OSX::UIApplication irb(main):003:0> OSX::UIApplication.sharedApplication #<OSX::UIApplication:0x1eefba class='UIApplication' id=0x26f6d0> irb(main):004:0> OSX::UIApplication.sharedApplication.delegate #<MyAppDelegate:0x1f8e70 class='MyAppDelegate' id=0x29dea0> # 得られたっぽいので、appに保存 irb(main):005:0> app = OSX::UIApplication.sharedApplication.delegate #<MyAppDelegate:0x1f8e70 class='MyAppDelegate' id=0x29dea0> # UIWindowのインスタンス irb(main):006:0> app.window #<OSX::UIWindow:0x1f8c40 class='UIWindow' id=0x2c0080> # UILabelのインスタンス irb(main):008:0> app.textView #<OSX::UILabel:0x1f8902 class='UILabel' id=0x2e2e00> # ウインドウの背景色を白にしてみる irb(main):009:0> app.window.setBackgroundColor OSX::UIColor.whiteColor nil # ラベルが見えないので、黒色にしてみる irb(main):012:0> app.textView.setTextColor OSX::UIColor.blackColor nil # なんかさびしいので画像とかネットからとってくるか irb(main):013:0> gif = open('http://www.ruby-lang.org/images/logo.gif','rb') {|f| f.read } Errno::ENOENT: No such file or directory - http://www.ruby-lang.org/images/logo.gif from (irb):13:in 'initialize' from (irb):13:in 'open' from (irb):13 # open-uriがないんですねわかります irb(main):014:0> require 'open-uri' true # とれた irb(main):015:0> gif = open('http://www.ruby-lang.org/images/logo.gif','rb') {|f| f.read } "GIF89a...." # 画像データからUIImageをつくる irb(main):016:0> img = OSX::UIImage.imageWithData(OSX::NSData.dataWithBytes_length(gif, gif.length)) NameError: uninitialized constant OSX::UIImage from (irb):16 # UIImageって何だよっておこられたので、ns_importする (NameError出た時はこれを試してみてください) irb(main):017:0> OSX::ns_import :UIImage OSX::UIImage # えー、NSDataもないの??? irb(main):018:0> img = OSX::UIImage.imageWithData(OSX::NSData.dataWithBytes_length(gif, gif.length)) NameError: uninitialized constant OSX::NSData from (irb):18 # はい irb(main):019:0> OSX::ns_import :NSData OSX::NSData # UIImageできた irb(main):020:0> img = OSX::UIImage.imageWithData(OSX::NSData.dataWithBytes_length(gif, gif.length)) #<OSX::UIImage:0x1e23f0 class='UIImage' id=0x287850> # UIImageViewをつくってみるよ irb(main):021:0> imgview = OSX::UIImageView.alloc.initWithImage img NameError: uninitialized constant OSX::UIImageView from (irb):21 # またかよ irb(main):022:0> OSX::ns_import :UIImageView OSX::UIImageView # できた irb(main):023:0> imgview = OSX::UIImageView.alloc.initWithImage img #<OSX::UIImageView:0x1d1af0 class='UIImageView' id=0x31c48d0> # とりあえずwindowにaddSubviewしてみる irb(main):026:0> app.window.addSubview imgview nil # なんか上の方にでて微妙なので、frameをいじろうかな irb(main):028:0> imgview.frame #<OSX::CGRect:0x376dec @size=#<OSX::CGSize:0x376ba8 @height=119.0, @width=331.0>, @origin=#<OSX::CGPoint:0x376d38 @y=0.0, @x=0.0>> # こんなんでどうだ? (値はあらかじめ試行錯誤してます) irb(main):030:0> imgview.setFrame [-5,140] + imgview.frame.size.to_a nil # テキストの位置も悪いなあ、こんなんでどうだ? (値はあらかじめ試行錯誤してます) irb(main):034:0> app.textView.setFrame [0,260,320,40] nil # テキストをちょっと変えるか irb(main):035:0> app.textView.setText 'with RubyCocoa' nil # フォントもちょっと大きく irb(main):042:0> app.textView.setFont OSX::UIFont.boldSystemFontOfSize(32) nil
iphone-rubycocoa プロジェクトの進捗状況とか今後とか
大分前に eval.rb over telnetが動いていたのですが、この irb over telnet はなぜか上手く動かない(特に、例外が発生したりすると、longjmpのあとに、必ず死んでしまうという)謎バグがありました。この原因がようやく分りました。どうやら longjmp() が原因ではなく、そのあと、 sigprocmask() を行った時に落ちていたようです。signal は iPhoneアプリにおいて、そもそも使わないでも良いと思うので(複数プロセスも使わないですし)、ばっさり signal support 自体を削って irb + telnet ができるようになりました。いまのところ安定しているようで、いい感じです。そろそろtest/以下のテストも始めようと思います。
それから、RubyCocoaの方ですが、libffiのほう、どうやらiPhone実機で動いているという話もあり、BridgeSupport相当がちゃんと動いてない現状版をメンテするよりは、最新版(0.13.2)の移植をちゃんとやったほうが良いかなと思いはじめていて、次にこれをやろうと思います。最新版が入れば(BridgeSupportもあるので)もうちょっとまともにRubyCocoaでアプリが書けるようになるのではないかと思います。
あと上のサンプルですが、RubyCocoaで書かれたクラスを動的変更していますが、これは自分でObjCで書いたクラスも同様のはずなので、それをできるようにしてみようかと思います。