はじめに
このエントリは第14回yidevでもお話させていただいた内容になります。
スライドはこちらだけど、多分大事なことは口頭で話てしまったのであんまり参考にならないかも(汗
長々と書いてもアレなので、まずは結論から。
NSInvocation の getReturnValue を使うときは自分で Retain Count をマネジメントすること!!
サンプルコード
Githubにサンプルコードを置きました
この記事はサンプルコードをベースに説明が書かれているので、こちらのコードをダウンロードしてください。
NSInvocationについて
基本的な使い方やパラメータの渡し方についてはサンプルやこちらの記事を参考にどうぞ。
NSInvocation を使ってクラスメソッドを呼ぶ
なんでNSInvocation?
今回私がNSInvocationを使うことになったのは、リーダブルコードを読んで影響をうけて、コードの冗長性はなるべく排除していいコードを書きたい!と思うようになったから。
とはいえ、そもそもNSInvocationが必要になるシーン自体もそこまで多くはないのでその前に設計について考えてみることも大事です。
NSInvocation を使う前
話がそれましたが、NSInvocation の getReturnValue について。
まずはサンプルの SimpleChapterViewController.m, SimpleDAO.m をご覧ください。
Chapter <-> Section <-> Paragraph という構造を持つデータベースからデータを引っ張ってきてテーブルに描画するサンプルとなっています。
だた、SimpleDAO.m を見て分かる通りかなり記述が冗長なのと、どんなデータを取りたいというSQLの部分が目立たない感じの記述になっています。
NSInvocation を使った後
InvokedDAO.m を見てください、だいぶスッキリしました。
ただ、InvokedChapterViewController.m を表示させようとすると落ちてしまいます。
どこかと思って Instruments で探ってみると、NSInvocationの返り値で取得したNSDictionaryがReleaseされているのが原因でした。
// 注:サンプルではここのフラグを false に変えると正常に動作するほうに分岐されます
if (true) {
NSDictionary *dataDictionary;
// invocate method by NSInvocation.
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"%@:", factoryMethod]);
NSMethodSignature* signature = [[self class] methodSignatureForSelector:selector];
NSInvocation* invocation = [ NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setTarget:[self class]];
[invocation setArgument:&statement atIndex:2];
[invocation invoke];
[invocation getReturnValue:&dataDictionary];
[result addObject:[NSDictionary dictionaryWithDictionary:dataDictionary]];
}
よくよくAppleの公式ドキュメントを見ると、たしかに Retain とかしねーよ?って書いてあります。
(今回の件は arguments じゃなくて returnValue のほうなのだけど、どうやらそれも同じっぽい)
This class does not retain the arguments for the contained invocation by default. If those objects might disappear between the time you create your instance of NSInvocation and the time you use it, you should explicitly retain the objects yourself or invoke the retainArguments method to have the invocation object retain them itself.
retainArguments について言及されていますが、今回は返り値のみが retain されればいいので、CFRetainを使うように記述したところうまくいきました。
参考文献:StackOverFlow
CFTypeRef data;
// invocate method by NSInvocation.
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"%@:", factoryMethod]);
NSMethodSignature* signature = [[self class] methodSignatureForSelector:selector];
NSInvocation* invocation = [ NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setTarget:[self class]];
[invocation setArgument:&statement atIndex:2];
[invocation invoke];
[invocation getReturnValue:&data];
// When out of this method scope, then released data. So, use CFRetain and go well.
// http://stackoverflow.com/questions/7078109/why-does-nsinvocation-getreturnvalue-loose-object/11569236#11569236
if (data) CFRetain(data);
NSDictionary *dataDictionary = (__bridge_transfer NSDictionary *)data;
[result addObject:[NSDictionary dictionaryWithDictionary:dataDictionary]];
Instruments を実行してみても、ちゃんと画面遷移に応じてインスタンスが確保されて画面が消えるとメモリ解放されているのがわかります。
おわりに
Appleの公式ドキュメントはよく読もう!w
そして リーダブルコード!!