Debugログについて

iphone開発で、デバッグログを出したいことはよくある。NSLogという標準のログ出力関数があるのだが、これだけでは機能不足。
この NSLogは Debug、Release関係なく出してしまう。また、呼び出し元情報等もないので、自分で書いてやる必要がある。
なにかいいロガーライブラリはないものか、と探し見てみたが、不思議とあまり見つからない。しょうがない、自作でロガーを作るか、と作り始めたところで、下記を見つけた。結果、これで全て問題なし。

そうか、わざわざロガークラスとか作らなくても、マクロでいいか。。。Java屋なのでlog4jみたいなの、とつい思ってしまっていた。C言語というのを意識しないとすぐ忘れる。

NSAutoreleasePool はどこまでやってくれるのか

iphone開発ではガベージコレクションは使えないため、alloc/releaseを使って自分でメモリ管理を行う必要があるのはどの入門書にもあります。また、それを便利にするための NSAutoreleasePool というのがあり、それもよく使います。しかし、この NSAutoreleasePool はどこまで面倒を見てくれるイマイチよく分からなかったため調べてみることにしました。

はじめに NSAutoreleasePoolのおさらい。NSAutoreleasePoolのインスタンスを alloc 、init で作成して使いますが、iphoneの場合、プールが最初に作られるので明示的にプールを生成しなくても使えます。プールにオブジェクトを登録するには NSObjectの autorelease メソッドを使います。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  // プールを明示的に作成
  NSObject *obj = [[[NSObject alloc] init] autorelease];  // autorelease でプールにオブジェクトを登録
  ・・・
  [pool release];  // プールをリリースすると、登録されたオブジェクトもリリースされる

ここまではよく解説書にあります。プールをリリースしたときにどういったことが行われるのでしょうか。プールに登録されたオブジェクトに単に1度だけ release を送っているのか、または完全に解放してしまうのか。

NSAutoreleasePool を release するとどうなるか

実際にやってみましょう。まずは一般的な動きから。オブジェクトの参照カウントを retainCount で取得して表示しています。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[NSObject alloc] init] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool release];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
pool released.
obj retainCount: 1
obj description: 

いきなり予想外!2回目の [obj retainCount] のところでは既に解放されているため、メモリ違反で落ちるはずが、きっちり動いている。しかも retainCount 減ってない!!
実は [pool release] に問題がありそう。リファレンスを読むと、release メソッドは リファレンスカウンタ環境では動くが、GC環境では何もしないため、通常は drain メソッドを使え、とあった。iOSGC未サポートだから release で問題ないと思うのだが、シミュレータは違うのだろうか・・・ なんにせよ、drain メソッドに置き換えると期待通りに動く。

■修正版

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[NSObject alloc] init] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool drain];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
pool released.
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。

プールを開放した後で、オブジェクトのretainCountメソッドを呼ぶとメモリ違反が起きてます。ちゃんと解放されてますね。

NSAutoreleasePool を drain するとどうなるか

ということで、気を取り直して本題。明示的に retain した場合、プールを開放したときにそれらのオブジェクトはどうなるかが知りたかった。そこで retain して参照カウントを増やしてからプールを開放(drain)してみる。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[NSObject alloc] init] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [obj retain];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool drain];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
obj retainCount: 2
pool released.
obj retainCount: 1

プールを開放しても、オブジェクトは解放されてない。どうやら、autorelease したオブジェクトに対して release を一度だけ送っているようだ。まあそうだろう。そうでないと、他のオブジェクトのプロパティなどにセットしたものも全てreleaseされてしまい、あちこちでメモリ違反が起きそうだ。

今度は autorelease を2度呼ぶとどうなるかを試してみる。自分で alloc してインスタンス化したオブジェクトはいいが、そうでない方法でオブジェクトを取得したとき、そのオブジェクトが autorelease されているかどうか分からないことがある。そういう場合、もう一度 autorelease を呼んで問題があるか知りたい。先のコードの autorelease を二つにしてみる。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[[NSObject alloc] init] autorelease] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [obj retain];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool drain];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
obj retainCount: 2
pool released.
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。

先程はプールを開放しても retainCount が 1 残っていたのに、今度はメモリ違反になった。メモリが解放されているようだ。どうやら autorelease を呼んだ回数だけ、解放時に release が送られるようである。ということで、autorelease されてるか分からないからもう一回よべばいいや、というのはとんでもない結果をもたらしてしまうことが分かった。

まとめ

  • NSAutoreleasePool の解放は drain を使うこと。
  • autorelease を呼んだ回数だけ、後で release が送られる。
    • retain したものは残るため、自分で retain したものは自分で release すること!
      • プロパティなどにセットしてretainした場合は、そのオブジェクトの dealloc でしっかりreleaseする!
    • alloc以外で取得したオブジェクトを勝手に autorelease しない!
      • ちゃんと autorelease されていることを信じるしかない。
      • autorelease されているかどうか判定する方法はない??

おまけ

NSDictionaryを使う時に気になったこと:定数定義とクラスキャスト

iOSフレームワークには、NSDictionary といういわゆるハッシュマップのようなキー/バリューストアとして使えるものがある。これを使っていて気になったことをメモ。

予め決まったキーを使う場合、普通それを定数としておきたいと思う。定数を定義するのには define ではなく const を使う、というのは以前の通り。しかし、以下のような書き方では警告がでる。

const NSString *key = @"someone";
・・・
id *value = [dictionary valueForKey:key]
(ここで警告:warning: passing argument 1 of 'valueForKey:' discards qualifiers from pointer target type)

これは、const を書く順番に問題がある。

const NSString * key = @"..."; // pointer to const
  or
NSString const * key = @"...";

上記の書き方は、const な NSStringオブジェクトへのポインタとなる。 [dictionary valueForKey:] で引数にとる型は、(NSString *)key のため、ここで型の不一致が起きてしまう。それを回避するためには以下のように定義する。

NSString * const key = @"..."; // const pointer

これは、NSStringオブジェクトへのポインタの const となり、型は (NSString *) のため、警告は出ない。なお、前述の書き方でも別に動かないわけではない。なら警告ださなくても、と思わなくもない。正直なところ、この二つの違いがどう影響するのかまでよくわかっていません。。。

ちなみに、グローバル変数としてkeyを定義すると、他のソースファイルに影響することがある。そういう場合は、static をつけることで、定義しているファイル内からだけアクセスを許可するようにできる。逆に、他のファイルと共有したい場合は static はつけてはいけない。

(追記:他のファイルから読み込む場合、ヘッダファイル中では extern で宣言だけ行い、定義は実装ファイルで行うようにする。)

ところで、最初のサンプルコードのように id 型で返ってきたオブジェクトを適切な型にキャストしたことが多々ある。Objective-C でクラスキャストの方法が分からなかったので調べた。普通に(型名)で良いようだ。ただし、オブジェクトはポインタなので、ポインタ演算子*をつけること。
例)

  id *value = [dictionary valueForKey:key]
  MyClassA castA = (MyClassA *)value;

#import の使いどころ:循環参照しないために。

!注意!
この内容はあまり正しくありません。Objective-Cの循環参照についてとして、書き直しました。こちらを参照ください。


Objective-C を書いていてたまに出会うのが、循環参照の問題。

error: expected specifier-qualifier-list before 'MyClass'

循環参照していると、こののように定義が無い、という旨のエラーが出る。
ClassA と ClassB という二つのクラスがあるとする。それぞれ相互にヘッダの中で import すると、この循環参照が発生する。

[ClassA.h]
#import "ClassB.h"

@interface ClassA {
  ClassB variableB;
}
@end
[ClassB.h]
#import "ClassA.h"

@interface ClassB {
  ClassA variableA;
}
@end

これを解決するには、ヘッダではなく、実装ファイル(ClassA.m、ClassB.m)のほうで import する。しかし、それでは ヘッダ中のクラス名が解決できないため、ヘッダには @class ディレクティブでクラス名であることを宣言する。

[ClassA.h]
@class ClassB;

@interface ClassA {
  ClassB variableB;
}
@end


[ClassA.m]
#import "ClassB.h"
...
[ClassB.h]
@import ClassA;

@interface ClassB {
  ClassA variableA;
}
@end

[ClassB.m]
#import "ClassA.h"
...

さて、ここからが本題。

そもそも、ヘッダファイル中にヘッダの import を書くから循環参照が起きてしまう。ならば、最初からヘッダファイルではなく、実装ファイルの方で import すればよいのではないか。それで、必要に応じてヘッダファイル中で、@class や @protocol ディレクティブで宣言すれば良さそう。
ただし、基盤フレームワーク、UIKit/UIKit.h みたいなのは ヘッダファイルで import してもよいかもね。循環参照の恐れは無いし、いちいち @class で宣言するのはめんどくさい。

ということで、まとめ。

  • 自分が作ったクラス、プロトコル等を他のクラスで使用する場合は、実装ファイルで import する。
  • ヘッダファイルで出てくる場合 @class, @protocol で宣言する。
  • フレームワーク等の、循環参照しないものは ヘッダファイルで import する。

define か const か?

Objective-C を書いていてたまに気になるのが、タイトルの件。
定数を使用するときに、 define を使うべきか、 const を使うか。

Objective-C でどうするのがよいか、というのはあまり見つけられなかったが、C++ ではおおよそ、const を推奨しているようです。
理由を見ると、Objective-C でも同様に const を使用した方がよさそうですね。

主な理由は以下の4点

  • 型がはっきりする
  • 名前空間の問題
  • 変更不可にするため
  • 間違い防止
型がはっきりする

define だと、型が無いため、なんにでも代入可能になってしまう。const で明示した方が安全。

名前空間の問題

const では スコープが決められるため重複の心配が少ない。また、内部仕様変数のアクセス制限もかけられる。defineではグローバルスコープになってしまうため影響範囲が大きくなる心配がある。

変更不可にするため

define で定義した物は undef などで上書きされる可能性もある。

間違い防止

defineで展開した結果、計算順序の優先度で意図しない展開になることを防ぐ。

    • -

ちなみに、フラグとして定数を使う場合は enum がありますが、上記の二番目、名前空間の問題を考えると const のほうがいいのかもしれない。が、数が多いと enum のほうが便利だよなぁ。


参考:http://www.geocities.jp/ky_webid/cpp/language/020.html

XIBを使うべきか、使わざるべきか

iphoneアプリの開発では、XIB(XML Interface Builder)というGUIエディタで画面を設計できます。別にこれを使わなくても、自分でソースをガリガリ書いて画面を組むこともできます。
というか、むしろ自分で書いた方が何をどう連携させるのか分かりやすいとか、XIB だけで出来ないことも多く、結局コードも結構書く必要があるため、ならはじめから IB イラネ(゚⊿゚) ってことも往々にしてある。

しかし、ほんとに XIB は使わなくてよいのだろうか。XIBを使用するメリット/デメリットを考えてみる。

XIBのメリット/デメリット

メリット
  • 画面を見ながら設計できるのでイメージしやすい
  • コーディング量を減らせる
  • iPhone用とiPad用を別々のXIBを使用することで、ユニバーサルアプリケーションとして対応可能
デメリット
  • 余計なオブジェクトの生成が行われる。
  • 起動時間、メモリ使用量が多少増える
    • しかし最近はあまり変わりはない?
  • 中で何が行われているのか分かりにくい
注意点
  • 複数のXIBファイルに分けることで、全体のロードを早くする。

結論

ケースバイケース、と言ってしまえば身もふたもないが、やはり一番最初の基本的なデザインはIBを使った方がだいぶ楽だと思う。
IBをメインで組み立てていき、ちょっとしたものはIBを使わなくてもいいんじゃないだろうか。
どちらかに縛るというのは、一見効率良さそうでそうでもないかも。