|
|
2. ガベージコレクション
ガベージコレクションは Objective-C 2.0 で新しく追加された機能の中で最も筆頭にくるものみたいです。ガベージコレクションは従来のメモリー管理「リファレンスカウント方式」の面倒さと記述ミスから起こるエラーからプログラマーを解放してくれます。
まず始めに、第一回で作った object.m を Apple の指針に従って Book.h 、Book.m 、main.m の3つに分割しておきたいと思います。分割したファイルは book2 というフォルダにまとめることにします。
まず object.m の5行目の @interface から20行目の @end までをコピーして次のように Book.h を作ります。
Book.h
1行目に #import <Foundation/Foundation.h> を書き加えます。
次に object.m の 22行目の @implementation から55行目の @end までをコピーして Book.m を作ります。
Book.m
今度は1行目に #import "Book.h" を書き加えます。
最後に object.m の57行目の int main(void) { から最後までをコピーして新しい main.m を作ります。
main.m
そして今度は1行目に #import "Book.h" 、2行目に #import <stdio.h> 、3行目に #define MAXLENGTH 100 を書き加えます。6行目は前回の object.m では char ss[100]; としていましたが、ここでも ss 配列の要素数の指定に MAXLENGTH 記号定数を使って char ss[MAXLENGTH]; と記述するようにいたしました。
なおサンプルファイルは
book2.zip
からもダウンロードできます。
ダウンロードした book2 フォルダをホームフォルダにでも置いていただいてターミナルを起動します。もちろんすべてを手書きしてもらうことにこしたことはありません。
cd book2
などで main.m Book.h Book.m のあるフォルダに移動します。
コンパイルコマンドは
gcc main.m Book.m -o book -framework Foundation -wall
です。
実行は
./book
です。
実行画面は前回の場合とまったく同じになると思います。
旧メモリ管理 リファレンスカウンタ方式
では main.m の9行目
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
と33行目の
[pool drain];
の先頭に // を付けてこの2行をコメント化して無効にします。
そして再び
gcc main.m Book.m -o book -framework Foundation -wall
でコンパイルして
./book
で実行します。
上記のようにエラーがズラリと表示されると思います。エラーの内容は「メモリ管理に使う NSAutoreleasePool がないのでメモリー漏れが起こっている」という警告です。
このプログラムではプログラムを動かす過程で次々と作られるオブジェクト(この場合では NSString のオブジェクト) の破棄 (メモリー解放) を NSAutoreleasepool を使ったリファレンスカウンタという方式に任せる形になっています。しかし肝心の NSAutoreleasepool をなくしてしまったのでメモリーの解放が行われなくなってしまったのです。
このメモリー解放についてのリファレンスカウンタ方式はかなり面倒な方式であり、また間違いも起こりやすいので以前から違う方式でのメモリー管理の搭載が望まれていました。しかし一方で CPU への負荷が少ないなどの利点もある方式ではあったみたいです。
メモリー解放の必要性と仕組みについては後ほどサンプルを利用して再度説明しますが、メモリー領域はプログラムを動かす過程で次々と消費(プログラムによって確保)されていきます。そして不要となったメモリー領域は順次解放していかないとやがてメモリー領域が足りなくなってプログラムにもシステムにも悪影響がでてくることになります。
ガベージコレクション
そこで Objective-C 2.0 ではメモリー管理にリファレンスカウンタ方式に加えてガベージコレクション方式も使えるようになりました。ガベージコレクションは Java をはじめ多くのプログラミング言語ではすでに採用されており、今回の Objective-C 2.0 での大きな売りの1つとなっています。
では main.m の9行目と33行目をコメント化したまま(無効にしたまま)、次のコンパイルコマンドでコンパイルして実行してみてください。
gcc main.m Book.m -o book -framework Foundation -fobjc-gc-only -wall
実行は、
./book
です。
今度は正常に実行さたことだろうと思います。コンパイルコマンドの中の -fobjc-gc-only というオプションでガベージコレクションを有効にしています。ガベージコレクションを有効にするとメモリー管理についてのコードは一切記述する必要がなくなります。
ガベージコレクションのコマンドオプションには次の2通りがあります。
-fobjc-gc
リファレンスカウンタ方式とガベージコレクション方式を混在させる場合
-fobjc-gc-only
ガベージコレクション方式のみを使用する場合
なお、上記の2つはターミナルでのコマンドラインからガベージコレクションを有効にする方法ですが、通常の GUI アプリケーションの場合には下記の 図:target のように「ターゲットの情報」画面で設定いたします。
図:target
通常、プログラムでは次々と新しい変数が宣言され、次々と新しいメモリー領域が確保されていきます。それに対して使われなくなった変数は破棄してメモリー領域を解放していかなければプログラム全体のメモリー領域、あるいは場合によってはシステム全体が必要とするメモリー領域が足りなくなり、プログラムやシステムに不具合が起こることがあります。このように必要のなくなった変数によって消費されているメモリー領域が増えていく状態のことをメモリーリーク ( memory leak メモリー漏れ) と言います。そしてこのメモリーリークを防ぐために必要のなくなった変数を自動的に破棄してその変数が使用していたメモリー領域を解放してくれるのがガベージコレクションです。
しかし、このことを不思議に感じる方もおられると思います。なぜなら C 言語の説明をしている learn C では「関数内で作られる変数は static などの指定のない限り関数が終了する時に自動的に破棄されメモリー領域は解放される」と書いているからです。このように自動的に消滅する変数のことを自動変数と呼びますが、Objective-C でも関数の中やメソッドの中で作られる変数は特に指定のない限り C 言語の場合と同じく関数やメソッドの終了時に自動的に破棄・解放される自動変数です。static 指定のある変数や関数外で宣言された変数(グローバル変数)は逆に自動的に破棄されると困るので明治的に破棄しないかぎりプログラム終了まで残しておくのが一般的な方式なのでメモリーリークとは関係ありません。
ではなぜメモリーリークが起こるのかを、サンプルプログラム book を先にすすめながら説明していきたいと思います。まずは book プログラムが次々と新しいオブジェクトを作成していけるように、「終了」という命令を受けとらない限りプログラムが無限ループを繰り返すようにいたします。このような仕組みは通常メインループやイベントループと呼ばれる部分で実現します。イベントループは GUI アプリケーションでは必須の部分ですが、この book のようにコマンドラインプログラムでもよく使われる仕組みです。このメインループ部分を含んだ loop.m というファイルを main.m を元にして作ります。
なお、プログラムの中で使われるすべてのメモリー領域は、変数として確保されていた領域も含めてそのプログラムを終了するとすべて解放されシステムに吸収されます。要するにプログラムを終了すればメモリーリークも解消されるということです。このことは知っておいてください。
メインループ event loop
loop.m
この loop.m ファイルは先ほどの
book2.zip
の中にも入っています。
このように main 関数の記述されているファイルを別の名前にしてもまったく問題ありません。むしろ プログラム名.m という名前にするほうが一般的です。
コンパイルコマンドは
gcc loop.m Book.m -o book -framework Foundation -wall
です。今回は main.m の変わりに loop.m を指定していることに注意してください。実行は
./book
です。
図:実行画面
プログラムを始めると「続行=1 終了=9」と表示されます。1と9以外が入力された場合は「続行=1 終了=9」という表示が繰り返されます。1を入力するとアドレスブックにデータを入力できるようになります。入力が終了するとすぐにそのデータが表示されます。1を選び続ける限りこの作業が繰り返されます。9を入力するとこのプログラムは終了します。
loop.m コード説明
今回のコード説明がこの learn ObjC 2.0 の中で一番大事な説明になるような気がします。
3行目 #import <stdlib.h> は19行目で使っている atoi 関数のヘッダファアイルです。
5行目 void enterBook(id book); はメモリーリークを起こす部分であるデータの入力と表示の部分を受け持つために新しく作った enterBook 関数のプロトタイプです。このデータ入力とデータ表示部分は元の main.m では main 関数の中にありました。自動変数の考え方では関数が終了した時点で変数も破棄されメモリー領域も解放されます。しかし main 関数の中にあっては関数が終了したのでメモリーが解放されたのかプログラムが終了したのでメモリーが解放されたのか区別できません。そこで enterBook 関数を作ってメモリーリークの原因となっている部分をそこに移しました。元の main.m の13行目から31行目を loop.m の中に新しく作った enterBook 関数の中に移動します。loop.m の36行目からがその部分です。
10行目で YES か NO だけを保持できるに BOOL という型の quit (終了という意味です) という変数を宣言しています。この変数は15行目からはじまる while 文の条件式として使います。まずループのはじまる前の14行目で quit を NO で初期化しておきます。15行目では ( ! quit ) と Not 演算子 ! を使って quit が YES でなければ(つまり NO なら)ループが続くようにしています。このような形で while 文を使う方式がイベントループとして最も一般的な方法です。さらに本来はこのループ部分を別関数にして main 関数から呼び出すのが最も正式というべき形ですが、今回は main 関数の中に直接メインループを設定しました。なお GUI をもつ Cocoa アプリケーションの場合には main 関数の中から NSApplicationMain というイベントループ関数を呼び出す形になっています。
12行目の book = [[Book alloc] init]; では自作クラス Book のインスタンスを作っています。従来のリファレンスカウンタ方式ではオブジェクトは alloc メソッドでメモリー上に確保された時点でそのオブジェクト自身が内部に持っているリファレンスカウンタの値が1に設定されます。この新しく作られたオブジェクトを他からも使いたい場合にはこのオブジェクトに対して retain というメッセージを送ります ( [anObject retain]; ) これでそのオブジェクトのリファレンスカウンタは2になります。この場合 retain メッセージを送ったほうをオーナーと呼びますが、オーナーがそのオブジェクトが必要なくなると今度はそのオブジェクトへ release メッセージを送ります ( [anObject release]; )、すると release メッセージを受けたオブジェクトのリファレンスカウンタの値が1減ります。このようにして最終的にリファレンスカウンタの値が0となった時にそのオブジェクトは自動的に自身の dealloc メソッドを呼び出して自分自身を破棄してメモリー領域を解放します。
ここまでの説明を読んだだけでも、すごく面倒な方式だな、という感想を持たれることと思います。面倒なだけでなくこのリファレンスカウンタ方式はミスも起こりやすい方式です。しかしガベージコレクションでは retain も release も、そして後で説明する autorelease メッセージも一切記述する必要はありません。逆にこれらのコードを記述しても不必要な場合にはまったく無視されます。またここでは book オブジェクトは最後まで必要となりますので特にメモリー解放の手だてなどは講じていません。
15行目の while 文については
learn C no 5 ループ文 の while 文の項を参照してください。
atoi 関数は引数の文字列を整数値に変換する C 言語の関数です。atoi 関数を使う場合には stdlib.h を #import しなければなりません。
16行目と17行目は特に説明の必要はないと思います。
19行目の switch 文については
learn C no 4 条件分岐 の switch 文の項を参照してください。
このように while 文のなかに switch 文 を設置して各イベントを待ち受ける形にする方法はイベントループのごく一般的な手法です。
NSAutoreleasePool と 一時的なオブジェクト
以上で main 関数の説明は終ったことにします。ここからがメモリリークの原因となる enterBook 関数の説明です。
32行目ではターミナルで入力された文字列を格納する変数 ss を宣言しています。そして34行目で NSAutoreleasePool のインスタンスを作っています。NSAutoreleasePool とは、後で解放したいオブジェクトを作った場合に [anObject retain] とする変わりに [anObject autorelease] とするとそのオブジェクトはこの NSAutoreleasePool に登録されます。そしてこの NSAutoreleasePool を release (解放) するとそこに登録されているすべてのオブジェクトに release メッセージが送られます。リファレンスカウンタ方式の記述を少しでも楽にするための手段です。
NSAutoreleasePool はその中にさらに NSAutoreleasePool を作ることができます。オブジェクトに autorelease メッセージを送った場合には一番直近に設置された NSAutoreleasePool に登録されることになります。
さて38行目の
[book setName:[NSString stringWitUTF8String:ss]];
では、NSString のクラスメソッド stringWitUTF8String を使って引数 ss つまりターミナルから入力された文字列を格納した変数 ss からUTF-8エンコード方式で NSString 文字列の一時的なインスタンスを作成します。そしてその一時オブジェクトを book オブジェクトの setName メソッドを使って名前データに格納しています。
この一時オブジェクト(一時的なインスタンス)はオブジェクト作成と同時に自動で NSAutoreleasePool に登録されることになっています。この一時オブジェクトもリファレンスカウンタ方式の面倒さを少しでも軽減するために作られたものだと思います。
すなわちリファレンスカウント方式では stringWitUTF8String メソッドで NSString オブジェクトを作る場合には必ず NSAutoreleasePool が必要だということです。NSAutoreleasePool を // などで無効にしてリファレンスカウント方式でコンパイルした場合にエラーが起こっていたのはこのためです。しかしこれでもまだ説明がつかない事があります。それは関数内の変数は関数が終れば破棄されるはず、ということです。
確かに関数内で作られた変数は関数が終了すると自動で破棄されます。しかし enterBook 関数が終了する時に破棄される変数は char型の ss のみです。stringWithUTF8String で enterBook 関数内につくられたのは新しい NSString 文字列のポインター(アドレス)です。NSString オブジェクト本体は関数内に含まれていません。その本体を管理しているのが NSAutoreleasePool です。ですからこの場合リファレンスカウント方式でメモリー管理をしている時に NSAutoleasePool が存在しなければ新しく作られた NSString オブジェクトのアドレスは永遠に失われてしまって(分からなくなって)しまい。プログラムを終了しない限りメモリーリークが起こったままになります。
そして今回のプログラムでは次から次へと book オブジェクトのデータを再入力できます。すなわちリファレンスカウント方式かガベージコレクション方式のどちらかのメモリー管理を採用しておかないと次から次へとメモリーリークが起こることになります。
cStringUsingEncoding
51行目の
printf("%s", [[book name] cStringUsingEncoding:NSUTF8StringEncoding]);
では、printf の変換指定子 %s の変換元としての C 言語の文字列を得るために book の name インスタンス変数に格納されている NSString オブジェクトに対して cStringUsingEncoding インスタンスメソッドを送って返り値として C 言語の文字列を取得しています。そしてその際に文字エンコーディングとして UTF-8 を引数として指定しています。それが NSUTF8StringEncoding ですが、これは一種の記号定数みたいなもので実質的には単なる整数の4です。試しに NSUTF8StringEncoding の部分をただの4という数字に書き換えてもコンパイルしてもまったく問題なくプログラムは動作します。
この cStringUsingEncoding: メソッドで作った C 文字列は、レシーバーである NSString 文字列が解放される時に同時に解放されるという非常に便利なメソッドです。一度ターミナルに文字列を表示した後でその文字列データがなくなってもターミナルの表示が消えるわけではありません。
これで NSString の stringWitUTF8String: メソッドと cStringUsingEncoding: メソッドの説明も終えることができました。どちらも引数まで含めると非常に長いシグネチャ (レシーバー名・メソッド名・引数名などの一連の文字列) になりますが Objective-C のクラス名・メソッド名・引数名は元から非常に長いものが多いです。慣れるしか仕方がありません。また推奨はできませんが先ほどのように NSUTF8StringEncoding という引数名をたんに数字の4と書き換えるという手もあります。
[pool release] と [pool drain]
56行目では、[pool release]; で NSAutoreleasePool を解放して、それに登録されているすべてのオブジェクトへ release メッセージを送っています。前回の main.m の中では [pool drain] と drain というメソッドで NSAutoreleasePool を解放していました。
この drain メソッドについて少しリファレンスを読んでみたのですが私の理解力ではよく分かりませんでした。分かったことは OS X 10.4 以降で有効なメソッドで、少なくとも release メソッドと同等の機能はすべて実装されていてさらにそれ以上の便利な機能も実装されているみたいです。この loop.m では従来の release メソッドを使いましたが、今後は OS X 10.4 以降であれば drain メソッドを使って大丈夫みたいです。ただし NSAutoreleasePool のインスタンスを解放するのに使うのは安全みたいですが、通常の retain でカウントアップされたインスタンスに対して release メソッドの変わりに drain メソッドを使っても大丈夫かは検証していません。というより drain (吐き出す) という意味からして NSAutoreleasePool の解放の時にだけ使うものだと思います。
以上、大変に長い説明になりましたが、このようにリファレンスカウンタ方式を使うことは大変面倒で気を使う作業です。せっかくガベージコレクション機能が搭載されたのですから、今後はすべてガベージコレクションを使ってサンプルを進めていくことにいたします
次回「宣言されたプロパティ」へ向けて
今回 loop.m では34行目で NSAutoreleasePool を設置して56行目でその NSAutoreleasePool を解放しています。これをもしガベージコレクションで管理した場合でも、これと同等のタイミングで設置と解放が行われているみたいです。もしかすると解放は47行目が終った時点で行われているかもしれません。かなり早いタイミングかつ正確なタイミングで解放を行うガベージコレクションみたいです。(言語によって搭載されているガベージコレクションにはかなり優劣の差があると聞いていましたが、たぶん Objective-C 2.0 で搭載してきたガベージコレクションはかなり高機能なもののようだと、素人考えながらも思います。
そのように感じる理由の1つが宣言されたプロパティ (次回掲載予定)機能で NSString *型のインスタンス変数の setter メソッドを自動合成した場合に copy メッセージを使っていることです。これはたぶん setter メソッドを呼び出した直後に、すぐに元のオブジェクトは解放されるからではないかと思います。
(もしかすると NSString *型インスタンス変数の setter は最初から copy メッセージを使うのが標準だったのを私が知らなかっただけかもしれませんが :-)
また今回 Objective-C 2.0 の新機能の紹介するのにまずガベージコレクションからはじめましたが、これはアップルの Over View でガベージコレクション→宣言されたプロパティ→高速列挙の順番になっているからではなくて、宣言されたプロパティ機能を使うにはガベージコレクションを使うことを前提としている部分がかなりあるみたいだからです。まだすべてを検証したわけではありませんが、ガベージコレクションを有効にしないとエラーの起こる「宣言されたプロパティ」機能はそこそこあるように感じます。
(というか今回作ったサンプルではガベージコレクションを有効にしないと宣言されたプロパティ機能は全滅でした)
もしかすると宣言されたプロパティ機能とはそういうものなんでしょうか。もう一度リファレンスをチェックしたいと思っています。
お疲れさまでした。これで「learn ObjC 2.0」の第二回は一応終ります。HTML の行数だけでも今までの中で一番長いものになりました。そしてまた書き直しが起こりそうな気もします :-)
|
|
|