The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

Objective-Cのメモリ管理はそんなに難しくはない(理論編)

Objective-C(非GCの場合)のメモリ管理は癖があってわかりづらいと聞いていたのですが、
実際にiOS開発でやってみたところ特にそうは感じず、むしろCとかC++よりは親切にすら感じました。

自分の理解の範疇でObjective-Cのメモリ管理について説明してみます。

ここでは一貫性を持たせるためにオブジェクトではなく、クラスオブジェクトとしてインスタンスという言葉で統一して使います。
また、allocを伴わない生成、autoreleaseについてはここでは触れず別途実践編で触れていきたいと思います。

インスタンスの生成・削除は基本的には全く同じ

C++の場合
生成・・・new
削除・・・delete

Objective-Cの場合
生成・・・alloc して init(またはnew)
削除・・・release (厳密にはreleaseの中でdeallocが呼ばれます※)
※なぜdeallocじゃないのかは後ほど説明します。

Objective-Cではallocではインスタンスを生成するだけで、初期化はinitでやるという点が違うくらいでほぼ一緒です。

ただし、実際にインスタンスが削除されるのはreleaseを呼んだ時点とは限らないというところがポイントになります。
これが混乱を巻き起こしていると思われます。
これを理解するには、なんでそんな作りになってるのということを理解する必要があるかと。

なぜObjective-Cでは、C/C++と同じやり方ではなく、後ほど説明する「retainカウント」という独自の方法をとったのでしょうか。

C/C++におけるメモリ管理の問題

まずはC/C++のメモリ管理のやり方ではどこがまずいのかを考えてみます。
  • 確保したメモリをどこが開放すべきなのか不明確である
これが一番の問題だと思っています。
インスタンスを生成した場合に、生成した人(クラス)が削除すれば問題ないわけですが、大抵の場合そうではなく、クラスのインスタンスのポインタをたらい回しにしているうちに、あれ、これってどこで削除するんだっけということになり、削除し忘れが起こったり、複数の箇所で削除しようとして落ちるといったメモリ関連の不具合が発生します。

これを解決するには、生成したものをどこで削除するかといったことの「決め事」を作る必要があります。そのやり方は様々あり、プログラムに求められる性能とかにもよるのでプロジェクトによって違います。
共通していることは、決め事は「プロジェクト毎に決める」ということで「言語仕様としては決められていない」という点にあります。

よく言うと柔軟な言語仕様ですが、悪く言うと無法地帯を産み出しているとも言えます。

Objective-Cのメモリ管理

そこで、Objective-Cではインスタンスの生成・削除の責任をはっきりさせるために、「オーナーシップ」という考え方のもと設計されています。

Objective-Cのオーナーシップとは、簡単に言うと、所有者が責任を持って削除するということです。

このオーナーシップを実現するために「retain」と「release」が存在します。

「retain」は、このインスタンスを所有しますという宣言みたいなもので、retainを呼ぶことにより、所有者となります。
また、インスタンスの生成であるallocしたときは自動的にretainされるので、allocを呼ぶことでも所有者になります。
所有者が「release」することにより事実上削除したことになります

インスタンスを所有している人がだれもいなくなったら、ようやくその時になって初めてインスタンス実際に削除されます。(上で事実上と書いたのはこのためです)
ここが実際にインスタンスが削除されるのはreleaseを呼んだ時点とは限らないわけになります。
なのでreleaseでなく、いきなりdeallocを呼んでしまうとまだ削除できる状態でないのに、deallocの終了処理が動いてしまうという問題があるので、deallocは使ってはいけません。

オーナーシップでは、所有者、つまりはinitした人とretainした人は、かならずreleaseしなければなりません。もしそうしなければretainとreleaseの対応関係が崩れてしまい、メモリリークや不正メモリアクセスが発生します。

Objective-Cでのこのオーナーシップの実装は「retainカウント」という方法で実現されています。
インスタンス毎にretainカウントというカウンタを持っていて、alloc時に1になり、retainされたら1を加算して、releaseされたら1を減算し、0になったら削除するという単純なものです。セマフォみたいなものです。

オーナーシップの副作用

オーナーシップにより、メモリの確保・開放が明確になり、素晴らしいことでありますが、もちろんこれによる副作用もあります。

それは主にプログラムの速度に影響します。

retainカウンタというのを制御しているので、C++に比べると余計な処理をやっていることとなります。retain,releaseの呼び出しによる余計なオーバーヘッドももちろんありますので、retainカウンタ有り無しでは性能に違いが現れます。

オーナーシップはあくまでポリシーでしかない

前述で「所有者、つまりはallocした人とretainした人は、かならずreleaseしなければなりません。」と書きましたが、これはあくまで「ポリシー」でしかありません。

つまり、initした人とretainした人がreleaseを呼ばなくてもコンパイル時にエラーになったりはしません。
プログラムの書き方次第では、

ClassA* objA = [[ClassA alloc] init];
ClassB* objB = [[ClassB alloc] init];
[objA send:objB];//ClassAのsendメソッド内で引数のobjBをretainする.
[objB release];//ClassAでretainしたのをここでrelease.
[objB release];
[objA release];

なんてことも動きさえすれば、平気でできてしまいます。

なので、いくらObjective-Cでオーナーシップの仕組みを提供していても、プログラマがちゃんと理解せずに使っていると、ただの読みづらいコードやカオスなコードになります。
そういう使い方をするのであれば、retain、releaseを使うのは放棄し、alloc,deallocでどうにかすべきだと思います。(ライブラリを使うことも放棄することになりますが・・)
まぁそれだったらCで書いたほうがいいですね。。

Objective-Cのメモリ管理がどうのこうのと文句を言っている人がいたら、これに当てはまっている危険性があるのでよくコードを見てあげたほうがいいと思います。


つまりはどんな道具(言語)も使い方次第です。
是非Objective-Cユーザーはオーナーシップを理解し、よいコードを書いていきましょう。

-----------------------------------------------------------------------------------------------------------
嘘とか間違いなど指摘していただけると幸いです。

Objective-Cの理解に役立つ本

11章にObjective-Cの設計者との談話が載っています。



Objective-Cの言語の本はこれしか読んだことがないので・・。