工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記 あと機械学習とかM5Stackとか

ARCについて色々実験(1)

はじめに

iPhoneアプリ開発を勉強する中で、一番の苦労の種となっているのはObjective-Cにおけるメモリ管理です。特に、頭を悩ませていたのがARC(Automatic Reference Counting)という方式についての理解です。

Objective-C 2.0 からは、C#のようにガベージコレクションも導入されているようですが、iPhoneというハードウェアの性能的な制約ゆえかiOS上では利用することが出来ず、iPhoneアプリを開発する上でARCとお付き合いすることは避けられそうにありません。しかし、C++C#などでは経験したことがない方式であり、どうも馴染みづらく、文法的なルールなどで良く分からない所もいくつか出てきました。

そこでARCについて勉強をするために、簡単なプログラムを書いて色々実験してみました。その内容や結果について、何回かに分けてメモしてみます(全てのプログラムはOS X 10.8.2、Xcode 4.5.2 という環境で動作確認しています)


alloc/init によるオブジェクトの生成

実験1-01

まずは、実験に用いたプログラムの基本形(以下、srcARC1-01とする)を示します。プログラムの作成にあたってはこちらの記事こちらの記事を参考にさせていただきました。

//srcARC1-01
//当然ながら、ARCをオンにしてのコンパイルが必須です
#import <Foundation/Foundation.h>

@interface myClass1 : NSObject{
    int num;
}
@end

@implementation myClass1

-(void)showNumber{
    NSLog(@"******** 私は myClass No.%iです。\n", num);
}

-(void)dealloc
{
    NSLog(@"******** myClass No.%i 解放\n", num);
}

@end


int main(int argc, const char * argv[])
{
    NSLog(@"ローカルスコープ開始");
    {
        id mc = [[myClass1 alloc] init];
        [mc showNumber];
    }
    NSLog(@"ローカルスコープ終了");
    
    return 0;
}

myClass1 という非常に単純なクラスを定義していますが、dealloc メソッドを上書きして、これが呼ばれたときにログを出力するようにしてあります。ここがポイントであり、ARCにおいてどのタイミングで myClass1 のオブジェクトが解放されたのかを知ることが出来ます。

(なおARCでは、dealloc メソッドを上書きする際には、その中で [super dealloc] を書く必要はありません、というより書いてはいけないようです。これに相当する処理は、コンパイラによって自動的に挿入されるそうです)。

srcARC1-01 を実行すると、以下のようなログが出力されます(実際には、NSLog では日付、時刻、プログラム名なども同時に出力されますが、以降では全て省略します)。

ローカルスコープ開始
******** 私は myClass No.0です。
******** myClass No.0 解放
ローカルスコープ終了

srcARC1-01 では、main 関数の中でローカルスコープを作成し、その中で id 型のローカル変数 mc を宣言しています。そして宣言と同時に、alloc/init メソッド で myClass1 のオブジェクトを生成して代入しています(インスタンス変数 num の値は0で初期化されます)。mc はそのオブジェクトを参照することになります。

ここでARCでは、ある変数がオブジェクトを参照するときには、デフォルトで「強い参照(強参照)」として扱われます。mc には参照に関する特別な指定をしていないので、mc はオブジェクトを強参照します(mc はオブジェクトのオーナーであるとも言います)。オブジェクトは、それを強参照している変数が存在する限り(オーナーがいる限り)保持され続けます。

そしてARCでは、オブジェクトを強参照する変数が存在しなくなったら(オーナーがいなくなったら)、その時点で dealloc メソッドが自動的に呼び出されて、オブジェクトが解放されるようです。mc はローカルスコープ内のローカル変数であるため、これを抜けるタイミングで消滅してオブジェクトに対する強参照も当然外れます。この時点で、このオブジェクトにはオーナーが全くいなくなるため、dealloc メソッドが呼び出されて「******** myClass No.0 解放」というログが出力されます。


実験1-02

では次に、srcARC1-01 の main 関数を以下の srcARC1-02 のように書き換えてみます。

//srcARC1-02
//当然ながら、ARCをオンにしてのコンパイルが必須です

//※ main 関数以外は、srcARC1-01 と同一

int main(int argc, const char * argv[])
{
    id mc = [[myClass1 alloc] init];

    NSLog(@"ローカルスコープ開始");
    {
        [mc showNumber];
    }
    NSLog(@"ローカルスコープ終了");
    
    return 0;
}

書き換えといっても、変数 mc の宣言をローカルスコープの外に出しただけです。これを実行すると以下のログが出力されます。

ローカルスコープ開始
******** 私は myClass No.0です。
ローカルスコープ終了
******** myClass No.0 解放

srcARC1-01 とはログの内容が代わり、「ローカルスコープ終了」の後でオブジェクトが解放されています。ここでは mc はローカルスコープの外で宣言されているので、当然ながらローカルスコープとは無関係に main 関数の最後までオブジェクトを強参照し続けます。そして、main 関数が終了するときに mc も消滅して強参照が外れるため、ここでオブジェクトが解放されるようです。


実験1-03

それでは、更に以下の srcARC1-03 のように main 関数を書き換えてみます。

//srcARC1-03
//当然ながら、ARCをオンにしてのコンパイルが必須です

//※ main 関数以外は、srcARC1-01 と同一

int main(int argc, const char * argv[])
{
    id mc2;

    NSLog(@"ローカルスコープ開始");
    {
        id mc = [[myClass1 alloc] init];
        mc2 = mc;
        [mc showNumber];
    }
    NSLog(@"ローカルスコープ終了");
    
    return 0;
}

srcARC1-01 と同様にローカルスコープの中で変数 mc を宣言して、生成したオブジェクトのインスタンスを代入していますが、その直後にローカルスコープの外で宣言した変数 mc2 に mc を代入しています。これを実行すると、以下のログが出力されます。

ローカルスコープ開始
******** 私は myClass No.0です。
ローカルスコープ終了
******** myClass No.0 解放

mc2 = mc という代入によって、mc が参照するオブジェクトを mc2 も参照するようになります。mc2 にも特別な指定はしていないため、この参照も強参照となります。そのため、ローカルスコープを抜けて mc が消滅しても、 mc2 がオブジェクトを強参照し続けているのでオブジェクトは解放されません。main 関数が終了し、mc2 も消滅するタイミングで初めてオブジェクトのオーナーが全くいなくなるため、ここで dealloc が呼ばれてオブジェクトが解放されるようです。


メソッドファミリという概念

実は、ここまでの実験内容は、どんなオブジェクトについても当てはまるわけではありません。

ここまでの実験は、全て以下のように alloc/init メソッドにより生成された(言い方を変えれば、alloc/init メソッドの戻り値として得られた)オブジェクトについてのものでした。

id mc = [[myClass1 alloc] init];

実はARCでは、alloc/init メソッドは「メソッドファミリ」というグループに属する特別なメソッドとして扱われます。メソッドが戻り値としてオブジェクトを返す場合には、メソッドファミリに属するメソッドとそれ以外のメソッドで、オブジェクトの状態に違いが生じます。

詳解 Objective-C 2.0 第3版*1によると、ARCでは「alloc」「copy」「mutableCopy」「new」「init 」という名前のメソッド、あるいはこれらのキーワードに小文字以外の文字が続く名前のメソッドが、メソッドファミリに分類されるようです。

例えば、initWithNumber: や initToMemory といった名前のメソッドは、init というキーワードの後ろに大文字が続いているため、initメソッドファミリとして認識されるようです。また、名前の先頭にアンダーバー(_)が付いていても構わない(無視される)ので、例えば _init という名前のメソッドもinitメソッドファミリとして認識されるようです。一方で、initialize や InitWithNumber: や sample_init のような名前のメソッドは、前述の条件に反するためメソッドファミリとしては認識されないようです。

そしてARCでは、メソッドファミリに属するメソッドが、戻り値としてオブジェクトを返す場合には、参照カウンタ方式で言うところの retain 操作が施されたようなオブジェクトを返すようです。そのオブジェクトを変数(強参照の)に代入すると、その変数がオブジェクトのオーナーとなってオブジェクトが保持されます。また、オブジェクトのオーナーが全くいなくなるとオブジェクトが自動的に解放されます。これまでの実験(alloc/init メソッドによるオブジェクトの生成)で見てきた通りの動作となります。

それでは、いずれのメソッドファミリにも属しないメソッドが、戻り値として返すオブジェクトはどうなるでしょうか。これは参照カウンタ方式で言うところの autorelease 操作が施されたようなオブジェクトを返すようです。メソッドを呼び出す側では必ず以下のように、自動解放プールを用意しておく必要があります。

@autoreleasepool{
    //この中でメソッドを呼び出して、オブジェクトを戻り値として受け取る
}

そしてメソッドの戻り値として得られるオブジェクトは、自動解放プールに登録されます。これについては、ARCについて色々実験(3) 以降で扱います。



以下、 ARCについて色々実験(2) に続きます。

*1:Objective-C を勉強する上で、非常に参考となる解説書です