2012年10月23日火曜日

ICS以降のXmlPullParserで起きる不都合とか

androidのXml#newPullParserで取得できるパーサはバージョンによって異なる。

具体的には、
  • Android3.2まではExpatPullParser
  • Android4.0以降はKXmlParser
---

もともとandroid SDKにはこの2種類のパーサが用意されていた。

Xml#newPullParserで取得できるExpatPullParserの挙動にはバグが多いため、XmlPullParserFactory#newPullParser()で取得できるKXmlParserを推奨しているサイトもあった

しかし、ほとんどの書籍やサイトでは両者を区別せず、呼び出し方が簡単としてExpatPullParserを推奨されていた。もちろん自分も今まで全く知りませんでした。(解析器周りは複雑すぎてよく分からないことが多い…)

ICSになって、バグの多いExpatPullParserは削除され、Xml#newpullParserでもKXmlParserのインスタンスが取得できるようになった。結果として、バグったパーサ上で正常に動いていたコードが、逆に動かなくなる…という現象が発生した。


その原因と対策方法はDevelopers Blogでちゃんと解説されていたんだけど、スルーしてたせいで完全にハマってしまった。

---

それとは別に、ICS以降のExpatPullParserでのみ起きる不都合が存在する。


それはxmlにBOM(Byte-Order-Mark)が付与されている場合に発生する。BOMとはエンコード方式を特定するための特殊なバイト列のことで、一部のテキストエディタではUTF-8で保存した場合に先頭にBOMを付与する。


上記はWindows標準メモ帳にてUTF-8で保存したファイルを、バイナリエディタで開いたもの。先頭にEF BB BFのバイト列が埋め込まれていることが分かる。しかし、メモ帳側からはそのことは分からない

ExpatPullParserはこのBOMを解釈できないのか、TEXTノードとみなすのか、文頭でnextTag()を呼ぶと例外が発生してしまう。3.2以前のパーサではBOMが存在しても無視していたので、この点では退化している。

まあandroidが悪いというよりも、後方互換性からBOMを放置するJava悪いという気がする。対処するには、xmlファイルを格納したStringの0文字目を識別する。String#charAt(0)が0xFEFFなら、String#subString(1)で除外。

---

2つの不都合の見分け方は、以下の通り。
  1. 例外の発生箇所がnextText()の直後であり、UnexpectedToken...というエラーメッセージでposition:START_TAGと書かれていた場合、パーサの変化が原因である。
  2. 例外の発生箇所がxml読み込み開始直後であり、UnexpectedToken...というエラーメッセージでposition:TEXTと書かれていた場合、BOM問題が原因である。

2012年10月20日土曜日

NSAutoreleasePoolの書き方三種類。

Objective-Cにはよくあることだけど、やり方が複数あってどれが正しいのか困る。

[NSAutoreleasePool release];

昔のやり方ではこう。

autolereaseメッセージが呼ばれたオブジェクトに、releaseが呼ばれる。

[NSAutoreleasePool drain];

ガベージコレクタとともに追加。

GC環境下ではGC実行開始の要求、MRC環境下では上述のreleaseと同じ効果。

GC環境でのコードとMRC環境のコードが混在する場合のために存在する。一般的にはreleaseではなくdrainを使用するべき、とある。

@autoreleasepoolブロック

ARCでNSAutoreleasePoolの直接使用が禁止になるとともに追加。上述のreleaseと同じ効果を持つ(ドキュメントを見る限りdrainと等価ではない?…んだけど、そもそもARCとGCが混在する環境の話なんざ気にしなくてよさそう)

MRCでも@autoreleasepoolの方が効率が良いために推奨されている。

つまり全て@autoreleaseでOK。

余談

NSAutoreleasePoolでは、initされてからrelease/drainが呼ばれるまでの間に制御が別の場所に移る(つまりrelease/drainが呼ばれないまま放置される)と、そのプールでのオブジェクトがリークする恐れがあったが、@autoreleaseではブロックを抜ける際に必ず遅延releaseが実行されることが保証される。

2012年10月5日金曜日

CAMediaTimingFunctionの謎。

そういえばIntrospectionで得たprivateメソッドの情報って書いていいんでしょうか。本に載ってたりするからたぶんNDA違反じゃないよね。

----

androidでイージング関数の生成の役割を果たすInterpolator(3.0以降はTimeInterpolator)は、独自にオーバライドしてその挙動を自由に操作できます。なのでレガシーアニメーションAPIで、円でもリサージュ曲線でも数学的に軌道を記述可能なアニメーションを簡単に実行できます。

一方のiOSではCAMediaTimingFunctionがそのポジションなんですけど、getInterpolationに相当するメソッドが存在しないので、2点の制御点を用いて表せるベジエ曲線が限界になります。

+functionWithControlPoints::::

普通のAPIだとセレクタにくどい説明ラベルが付いていることが多いのですけど(controlPointX:y:nextPointX:y:みたいな)、通常利用を想定していないのか何も書いてません。

始点を(0,0)、終点を(1,1)としたベジエ曲線の、2つの制御点を指定します。例えばオーバーシュートなら制御点1(0.25,-1.0)、制御点2(0.75, 2.0)とかでしょうか。この値をそのまま並べて、

[CAMediaTimingFunction functionWithControlPoints:0.25:-1.0:0.75:2.0];

とやると多分それっぽいイージングが作れます。イージング関数は調べれば出てきますが、それを制御点に落とし込むのは結構大変な気がします…。

----

getInterpolationに相当するメソッドはない、と書きましたが、実際には存在するのです。

Introspectionでメソッドを洗い出すと、

-(float)_sloveForInput:(float)

というメソッドが見つかります。

しかし実際にアニメーション実行時に呼ばれるのは、getControlPointAtIndex:values:だけです。getControlPointAtIndex:values:は、initで与えた制御点の座標となる4つのfloat値について、0~4のindexに対して、出力引数で返すだけのメソッドみたいです。

MacOS系では使われているものの、iOSでは使われていないメソッドなんでしょうか…。