2015/04/17

[Android]Serializableの限界

Androidでシリアライズを用いると、割と簡単にStackOverflowExceptionを出すことができます。

私が直面したのはマッチメーキングをするアプリの作成中でした。
プレイヤーとプレイヤーを組み合わせた対戦履歴のリストがあり、対戦はプレイヤーのペアを保持し、プレイヤーは自分の対戦を保持します。
リレーショナルデータベースではありがちな構造だと思います。

ObjectOutputStream.writeObjectの実装は対戦履歴を読みに行ってプレイヤーを見つけ、プレイヤーの持っている対戦履歴を見つけ、さらにその中のプレイヤーを読んで…と走査していくはずです。
なんのチェックもなければ無限ループに嵌ってスタックオーバーフローを起こすでしょう。
最初はこの無限ループに嵌ったのだと思っていました。
しかし、小さいサンプルや少ない組み合わせだとループを作ったつもりでもシリアライズが成功するので、理屈がわからずにしばらく悩んでしまいました。
おそらく、走査したオブジェクトのリストを持っていて、一度走査したオブジェクトを発見した時点で走査を切り上げる、という実装になっていると思われます。
それでも結局スタックオーバーフローに陥ってしまうのだと思われます。しかも、それほど高負荷をかけなくても。
スタックをどの位確保しているかは実装依存だと思いますが、私の手元のGalaxy S2 Android 4.0.2の場合、8人の対戦を2回戦するとスタックオーバーフローが発生します。
これは実用上無視できる規模ではもちろんありません。

本物のコードは少々複雑なので、問題を簡易化したサンプルを作って検証しました。
AとB、2つのクラスがそれぞれのインスタンスを保持しあう構造で、どこまでシリアライズが可能かを確認します。

class A implements Serializable{
 B b;
}
class B implements Serializable {
 A a;
}

A instance;

for(int i=0; i<128; ++i){
 A a = new A();
 instance = a;
 for (int j = 0; j < i; ++j) {
  a.b = new B();
  a.b.a = new A();
  a = a.b.a;
 }
}

ObjectOutputStream o = new ObjectOutputStream(openFileOutput("test",0));
o.writeObject(instance);
o.close();


これもGalaxy S2 Android 4.0.2の場合、i=15でスタックオーバーフローが発生します。

解決編に進む前に、もうひとつ面白い挙動を見つけたので、そちらも紹介しておきます。

件のマッチメーカーアプリは回転に対応しています。
回転した際にActivityが再構成されますから、データを引き渡さなければなりません。
アプリはプレイヤーのリストと対戦履歴のリストをどちらも保持するゲームのオブジェクトを持っていて引き渡します。
onSaveInstanceStateで保存して、onCreateで再生するという、一般的な処理です。

オブジェクトを引き渡すのに使うメソッドputSerializable()、引数はObjectではなくSerializableです。
なので私はすっかり旧Activityから新Activityにデータが引き渡される際にデータはシリアライズ>デシリアライズが行われていると思い込んでいました。
しかし、だとしたら、回転時にStackOverflowExceptionが起こってしかるべきです。
実際、例外は発生せず、データを引き継いでの回転が行われていました。(事実は、先に回転の実装を行い、実績があるので保存もシリアライズを使おう、と思って嵌ったのですが。)
ストリームへのシリアライズとActivity間のシリアライズにどんな違いがあるのか、やはりしばし悩んでしまいました。

デバッガで確認したところなんのことはない、回転後のゲームオブジェクトは回転前と同じidを持っていました。
シリアライズなどされずにインスタンスそのものが引き渡されていたのです。

さてこれらの実験からオブジェクトのシリアライズは小さい単位でやろうねとか、回転したときには同じインスタンスが渡されるよとか結論するのは絶対ダメです。
シリアライズで無限ループが起きていないのも、同一インスタンスが送られているのも、実装依存です。
スペックに実装を規定するような記述はありませんでした。
他の端末では無限ループになってしまうような実装がされているかもしれません。
他の端末では毎回まじめにシリアライズが行われているかもしれません。
タイミング依存で、我がGalaxy S2でもシリアライズをすることがあるかもしれません。
例えばActivityが隠されていて、結構な時間がたった後に再表示される場合とか、ありそうな話です。
上品プログラマーはスペックに記述されていないいかなる挙動も想定してはいけないのです。

結論です。
Android Javaにおいては、相互参照するオブジェクトをシリアライズしてはいけない、と考えるべきでしょう。
データはデータベースで管理して、JavaオブジェクトはIDだけを持っていなさい、というのが一番Android向きの設計なのだと思います。が、それは言わないお約束にして、私はこんな感じで解決してみました。

class A implements Serializable{
 transient B b;
 int id;
 void setB(B b){
  this.b = b;
  this.id = b.id;
 }
 
 boolean bind(B[] bs){
  for(B b : bs){
   if(b.id == id){
    this.b = b;
    return true;
   }
  }
  return false;
 }
}

class B implements Serializable {
 int id;
 A a;
 B(int id){
  this.id = id;// it must be unique
 }
}
クラスAは保存用のidとBの参照を持つというハイブリッド構造です。

Bの参照がtransient宣言されているので保存されない、というのがミソです。

これで必要以上の循環参照は発生しません。要は参照構造が途切れていればいいので、Bに仕掛けは入れません。



この仕組みだと、デシリアライズしたときtransient宣言された参照bに値は入っていませんから、bind()でidをもとにデータバインディングをやり直さなければなりません。



余談になりますが、C#には何種類ものシリアライザが用意されています。

代表的なもののうち、BinaryFormatterは相互参照しているオブジェクト群をシリアライズすることができます。Windows OSで動くことが事実上決まっているので、上記のStackOverflowのようなことは起きないと思われ、使用するのは安全だと思っています。

もう一方のXmlObjectSerializerは相互参照しているオブジェクトのシリアライズを試みるとエラーになります。不親切にも見えますが、自分にできないことは受け入れないというのは正しい振る舞いです。

Javaはさまざまなプラットフォームで動かなければいけない宿命を持っているので見極めが難しいのですが、今の振る舞いならXmlObjectSerializerと同じく最初からNotSupportedExceptionを出してくれる方がむしろ親切なのではないかと思いました。





2015/04/12

[Eclipse][Android Studio][Android]EclipseとAndroid Studioの比較

星取表を作って優劣を見える化するような情報を求めている方には申し訳ありません。
もっとぼやーっとした一般論です。

そもそも、Android StudioはAndroid専用のツールのようなので、私のように他のプラットフォーム用のJavaも書くエンジニアに強い魅力はありません。
Android Studioを導入したからと言ってEclipseを削除することはありません。
そういう意味では、Android Studioにちょっと不利なレビューになっているかもしれません。

EclipseもAndroid Studioもカスタマイズし放題のIDEなので、カリカリにチューンしてしまったらどっちがいいもないでしょう。なので、比較はデフォルト動作で行っていると思ってください。
上品プログラマとしてはよほどのこだわりがない限りそのツールのデフォルト設定で使うように心がけています
機種変更したり、他人のPCで作業したりしたときに困らないようにと身に着けた習慣です。
ひとり1ノートPCが当たり前の今では無用なこだわりかもしれませんが、私がこの仕事を始めたころはコード書くのは共有PCなんて時代だったのです。
また、デフォルト動作がそのツールの設計思想に一番馴染んでいて、設計者が一番使ってほしいスタイルに違いない、という思想もあります。

と、前ふりをしてなんですが、EclipseとAndroid Studioはすごく近い思想でできたツールのようで、見た目のデザインの違いを乗り越えると使い勝手はそんなに変わらないように思います。

一番ドラスティックな違いはリソースファイルの位置ですか。
Eclipseはプロジェクトのルートにsrcとresが並んでいましたが、Android Studioはsrcの下に階層が掘られて、ソースコードが格納されたjavaとresが並んでいます
この違いのため、残念ですがEclipseとAndroidで環境を共有することはできません。
リンクを作り直すのも面倒なので、GitHubなどのリポジトリは作り直しました。

比較するとEclipseはシンプルにわかりやすく、余計なことは極力行わないツール、Android Studioは効率の良い開発をするためにいろいろ気を回してくれるツールのようです。
Android Studioの気配が気に入るか、過剰に思うかで好みが分かれるのかなと思います。
無名クラスをラムダ式にたたんでくれるのはすごいなと思いましたが、気が付くまで気持ちが悪かったです。
私はC#もやるので気持ち悪いなぁと思いながら見過ごしていたのですが、Javaしか知らない人はびっくりするんじゃないんでしょうか。
リソースidを文字で置き換えて見せてくれるのも、便利なんだと思いますが、最初は気持ちが悪かったです。
どちらも私は、慣れれば使えそうなのでもうしばらく使ってみますが、気に食わなければ設定で無効にすることはできるでしょう、きっと。

Eclipseだとエラー行にカーソルを置くことで表示された修正候補はALT+Enterで起動します。
Enterをたたくのは結構勇気がいるので最初は戸惑いがありましたが、慣れると快適です。
予測変換について、Eclipseは一文字でも間違えるとギブアップしていたのですが、Android Studioは訂正すると付いてくる根性があります。

Eclipseはいい意味でも悪い意味でもやっていることが推論しやすくて気持ち悪さのない”ツール”でしたが、Android Studioは開発効率を上げるために様々な工夫が入っているツールなのかなと感じています。
24時間でアプリを組み上げる競技プログラマさんなんかはAndroid Studioに魅力を感じるのかもしれません。
上品プログラマとしては質素なEclipseが大好きなんですが、Android Studioに任せて最少手順でコードを書くという経験も捨てがたく、甲乙つけ難いところです。
公式ページがEclipseを削除してしまったので、新規の方がわざわざEclipseを使う必要はないと思いますが、Eclipseに馴染んでいる人が敢えて引っ越さなければならないモチベーションも今のところ感じられない、というのが2015年4月時点での結論となるでしょうか。

考えてみれば当たり前のことですが、EclipseもAndroid Studioも目的はAndroid用のコード生成なので、本質的にやることは変わりません。
昔は同じC++でもMFCとBoland Cでニュアンスが違っていろいろややこしかったりしたこともありましたが、そういうことはありません。
同じDalvik向けのクラスファイルを吐くエンジンを使っている2ツールに本質的な違いはないのです。
Android開発作業の中でツールに依存する部分というのは何%もないでしょう。そういう意味ではどちらを使うかというのは実に些細なお話だと思います。
ツールとしてEclipseを前提にしている情報がAndroid Studioでまったく使えなくなることはまずないでしょう。
所詮がところ同じコンパイラでDalvik向けのクラスファイルを吐く仕組みですから、言語としては同じものです。
すでにEclipseで開発している人がAndroid Studioに切り替えたからと言って、今までの知識が一夜にして無駄になることもありません。

結論、見るまえに跳べ。

[Android Studio][Android]Android Studioに引っ越す

牛にひかれて善光寺参り。
しばらくEclipseを使っていこうと思っていたのですが、PCが水没してしまいました。
作業環境を再構築しなければならなくなったのを機会にAndroid Studioを使ってみることにしました。

インストールするものは、公式ページからダウンロードしました。

https://developer.android.com/sdk/index.html

去年まではここからEclipseがダウンロードできたのに、前バージョンへのリンクすらありません。
前の記事でEclipseを推していた私ですが、あっという間にリンク先が切り替わってしまい、ご迷惑をかけた方がいらしたら申し訳なかったです。
なにがあったのか背景をご存知の方、教えてほしいです。

私の場合は前述のとおりPCクラッシュからのやり直しだったので、Javaのインストールから行いました。
Eclipse環境がある方はAndroid Studioのインストールをするだけで済むはずです。
ダウンロードしてインストーラに従ってボタンを押すだけでした。
Android ADTがパックになっていなかった時代のEclipse構築を思い返すと本当に簡単になりました。

続けて日本語化というステップを当然のように記述されているサイトも見受けられますが、上品プログラマーとしては英語のままで使われることをお勧めします。
開発ツールは最低限単語さえわかれば使えます。最低限の単語がわからないようならそもそもソフトウェアの開発に困るはずです。わかるくらいまでは勉強しましょう。
日本語化して余計なトラブルを呼び込むことはままありますし、日本語だけで仕事ができる時代じゃないんですよ、ほんとうに。

続けてEclipseのプロジェクトをAndroid Studioに変換します。
Android StudioのImportはEclipseをダイレクトに読み込めるようになっていますから、なんの心配もありませんでした。
ディレクトリ構造が変わり、Gradleなる意味不明のものががつっと加わりますが、そこらへんは当面お任せしておけばよい感じでした。
Gradleについてはいずれ知らなければならないもののような気がしていますが。

インポートできたので、ビルドしてみます。
私はここで軽くひっかかりました。
ここまでダウンロードの待ち時間を含めても30分程度で来てしまいましたし、まぁ、少しくらいはなにかありますよね。

エラーコードを読んでみると、くだんのGradleさんがお怒りになっている様子でした。正直少しやばいぁなと思いました。
エラーコードをそのまま検索エンジンにコピペすると、Android Studioのスタートアップに困っている人のためのページがいくつもヒットしました。
同じ思いをしている人って結構いるんだなぁと思うと少し気持ちが楽になりました。

残念ながらそのものずばりの解決策は見当たらなかったのですが、Gradle Scriptに問題があるらしいなと目星をつけてよく見直してみると、じつはなんのことはない、Android Studioがエラーの起こっている行を赤いアンダーラインで表示してくれていました。
私の場合はAndroidのライブラリが2つ宣言されていて、ダブってるよ?というものだったので、片方をコメントアウトすることで無事ビルドが通るようになりました。

build.gradle(Module:app)
dependencies {

//    compile 'com.android.support:support-v4:20.0.0'
    compile files('libs/android-support-v13.jar')
}

オリジナルのプロジェクトのターゲット設定に手を加えていたので、そのせいでAndroid Studioが誤認識したのだと思います。
わかってしまえば単純明快です。
私の場合、前の環境をロストしてしまっていたのでやりませんでしたが、Eclipse側のお作法を直してインポートしなおすという手もあるのかな、と思いました。
いずれにしてもいえることは、お作法通りに作っていればツールは何とかしてくれるものだということ。大抵の場合問題はコーダー側にあります。

別のプロジェクトでも別のエラーが出ました。
今度は冷静に情報を観察することができました。
今度もライブラリファイルに問題があるようでしたが、上の例のように2つ宣言されているというものではありませんでした。
問題の行のフローティングメッセージを見ると、バージョンが"20"では低すぎるようなことが書いていたので、21に書き換えたところ、ビルドできるようになりました。
素直にエラーコードとエラーメッセージを読めば対応できるみたいです。

私の感覚では、EclipseからAndroid Studioへの引っ越しは”簡単”でした。