Obj-C再入門・今日のハマり所 initWithCoder内ではretainし忘れに注意

今日もまた、自分の貧弱なObjective-Cの理解のせいでハマった。
今回の主役はinitWithCoderメソッドとretainメソッド。
こんなクラスがあるとする。

@interface Hoge{
  MyObj *obj;
}
@property(nonatmic, retain) MyObj *obj;
@end

@implements Hoge

-(id)initWithCoder:(NSCoder*)coder
{
  if(self = [super init]){
    obj = [[coder decodeObjectForKey:@"myObj"] retain]; //(1)OK
    self.obj = [coder decodeObjectForKey:@"myObj"];     //(2)OK
    obj = [coder decodeObjectForKey:@"myObj"];          //(3)これはNG
  }
}
...

initWithCoderってのは、Objective-Cのアーカイブという機能に関するメソッドである。アーカイブってのはオブジェクトをシリアライズしたりデシリアライズしたりするための仕組みで、Javaシリアライズみたいなもん。iPhone開発だと、たとえばなんかのデータをアプリ内のデータ領域に保存したい場合などに使う。

initWithCoderはデシリアライズのためのメソッド。アーカイブからデータを復元するときに、コンストラクタ代わりに使われる。

で、今回ハマったのはinitWithCoder内で、復元した値をプロパティにセットするところ。initwithCoder内では[coder decodeObjectForKey:]を使ってアーカイブから値を取り出し、それをプロパティにセットすればよい。

で、脳みそが足りない俺は、この処理を上記の(3)のような感じでコーディングしてしまったのである。これはまずい。なぜか?このままobjにオブジェクトを突っ込んでも、オブジェクト自体のリファレンスカウンタがゼロのままだからである。
なので、このobjは速攻でdeallocされてしまう、というわけだ(※)。なので、initWithCoderが完了して、いざobjプロパティにアクセスしようとすると、もう値は破棄されてしまっているので、おかしな事になる、というわけだ。


※この辺はあいまい。詳細Objective-Cによると「リファレンスカウンタがゼロになるとdeallocが呼ばれる」みたいな事が書かれているけど、この場合、どのタイミングでdeallocが呼ばれるんだろう?decodeObjectForKeyメソッドを呼び出し後のいつか?

なので、(1)とか(2)の方法で値をセットしてやる必要がある。

たぶん(1)の方法が一般的なのかな?ちゃんとretainを呼んでいるので、リファレンスカウンタは1になっているはず。

(2)でもOKっぽい。というのは、objは@property(.., retain)として宣言しているので、セッター(self.obj=)経由で値のセットを行えば、セッター内で勝手にretainを呼んでくれるから。でもコンストラクタの中で、self.obj=、と書くのはちょっと違和感があるような無いような。


勘違いしている点も多々あるかもしれないが、とにかく今日はこの辺りでハマった。今作ってるアプリについては、ちゃんとコードレベルで品質の高いものを作ろう!と意気込んではいるのだが、こういう初歩的なところで躓きまくり。参考書よめばきっちり書いてあるんだがな〜。やっぱり基本は大事です。

GCが使える環境がいかにラクなのかということを再認識した。

ああ、GC++!