工場裏のアーカイブス

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

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

ARCについて色々実験(3)の続きとなります。

メソッドファミリに属さないメソッドによるオブジェクトの生成(他クラスのメソッドの戻り値として)

実験4-01

srcARC3-01 に、新しいクラスである myClass2 を追加してみます。myClass2 はgetMyClass1: というメソッドを持ち、myClass1 のオブジェクトを alloc と initWithNumber: メソッドにより生成して返します(getMyClass1: メソッドは、いかなるメソッドファミリにも属しません)。すなわち、myClass1 のオブジェクトを、myClass2 という他クラスのメソッドによって生成してみます。これを srcARC4-01 として以下に示します。

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

//※ myClass2の追加、および main 関数以外は、srcARC3-01 と同一

@interface myClass2 : NSObject

@end

@implementation myClass2

-(id)getMyClass1:(int)n{
    NSLog(@"******** myClass No.%i 他クラスのメソッドで生成\n", n);
    return [[myClass1 alloc] initWithNumber:n];
}

@end

int main(int argc, const char * argv[])
{
    id mc2 = [[myClass2 alloc] init];
    
    NSLog(@"autoreleasepool 開始\n");
    @autoreleasepool {
        NSLog(@"** ループ開始");
        for(int i = 1; i <= 3; i++){
            NSLog(@"**** ループ%i週目 先頭\n", i);
            
            id mc = [mc2 getMyClass1:i];
            [mc showNumber];
            
            NSLog(@"**** ループ%i週目 末尾\n", i);
        }
        NSLog(@"** ループ終了\n");
    }
    NSLog(@"autoreleasepool 終了\n");
    
    return 0;
}

main 関数の最初に alloc/init メソッドで myClass2 のオブジェクトを生成しておきます(変数 mc2 に代入)。そして、srcARC3-01 ではコンビニエンスコンストラクタ(myClassWithNumber: メソッド)で myClass1 のオブジェクトを生成しましたが、その代わりに myClass2 の getMyClass1: メソッドを用いています。これを実行すると、以下のログが出力されます。

autoreleasepool 開始
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 他クラスのメソッドで生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 他クラスのメソッドで生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 他クラスのメソッドで生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放
******** myClass No.2 解放
******** myClass No.1 解放
autoreleasepool 終了

ログを見ると「他クラスのメソッドで生成」という文字列の表示以外は、srcARC3-01 と全く同じ実行結果が得られています。生成された myClass1 のオブジェクトは自動解放プールに登録されています。myClass2 の getMyClass1: メソッドは、戻り値が myClass1 のコンビニエンスコンストラクタと全く同様であり、またメソッドファミリに属していないという点も同様です。そのため、srcARC3-01 と srcARC4-01 の実行結果が同じになったのです。

あるクラスのメソッドで、他クラスのオブジェクトを返すような場合であっても、ARCではあくまでメソッドファミリの規則に従って、そのオブジェクトの状態が決まるようです。


自動解放プールの記述位置について

実験4-02

srcARC3-01 でも srcARC4-01 でも、main 関数では for ループ全体を自動解放プールで囲い込むようにしていました。そこで、逆に for ループの内側で自動解放プールを用いるよう srcARC4-01 を srcARC4-02 のように書き換えてみます。

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

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

int main(int argc, const char * argv[])
{
    id mc2 = [[myClass2 alloc] init];
    
    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);
        
        NSLog(@"autoreleasepool 開始\n");
        @autoreleasepool {
            id mc = [mc2 getMyClass1:i];
            [mc showNumber];
        }
        NSLog(@"autoreleasepool 終了\n");
        
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

srcARC4-01 とは異なり、for ループが1周するごとに、ループの先頭で自動解放プールのブロックが開始し、ループの末尾でブロックを抜けるようにしています。これを実行すると以下のログが出力されます。

** ループ開始
**** ループ1週目 先頭
autoreleasepool 開始
******** myClass No.1 他クラスのメソッドで生成
******** 私は myClass No.1です。
******** myClass No.1 解放
autoreleasepool 終了
**** ループ1週目 末尾
**** ループ2週目 先頭
autoreleasepool 開始
******** myClass No.2 他クラスのメソッドで生成
******** 私は myClass No.2です。
******** myClass No.2 解放
autoreleasepool 終了
**** ループ2週目 末尾
**** ループ3週目 先頭
autoreleasepool 開始
******** myClass No.3 他クラスのメソッドで生成
******** 私は myClass No.3です。
******** myClass No.3 解放
autoreleasepool 終了
**** ループ3週目 末尾
** ループ終了

ログを見ると、srcARC4-01(あるいはsrcARC3-01)の実行結果とは異なり、ループが1周するごとに新たな自動解放プールが開始しては終了し、それに伴って、その周のループで生成されたオブジェクトも解放されています。

getMyClass1: メソッドの戻り値として得られたオブジェクトは、その周のループで開始した自動解放プールに登録されると同時に、その自動解放プール内のローカル変数である mc に代入されます。そして、その周の自動解放プールが終了すると同時に mc も消滅する(すなわち、mc からのオブジェクトへの強参照が外れる)ため、このタイミングでオブジェクトが解放されるようです。


なお srcARC4-01 と srcARC4-02 では、表面的には両者の動作内容(ループの1周ごとに myClass1 のオブジェクトを生成し、そのメソッドを呼び出す)には違いがありません。おそらくループの回数がごく少ないときには、どちらの方式で自動解放プールを利用しても、大きな影響は生じないと思います。

しかし、ループ回数が非常に多い場合はどうでしょうか。srcARC4-02 の方式ではループの各周の末尾でオブジェクトは解放されます。しかし srcARC4-01 の方式ではループの各周ごとに、(ループの外側の)自動解放プールにオブジェクトが登録されて貯まっていきます。例えばループが一度に10000周する場合には、一時的とはいえ10000個のオブジェクトが貯まってしまいます。特にiPhoneアプリでは、限られたiPhoneのメモリを圧迫してしまい、最悪アプリが落ちる原因になりかねません。

そのため、メソッドファミリに属さないメソッドを用いて、ループの中で何度もオブジェクトを生成するような場合には、srcARC4-02 のようにループの中で自動解放プールを用いる方式のほうが無難だと思います。


自動解放プールの強制脱出

実験4-03

srcARC4-02 のようにループの内側で自動解放プールを用いる場合に、その中で break などの命令を実行し、ループを強制脱出したらどうなるか実験をしてみます。srcARC4-02 の main 関数に、if 文を追加して書き換えた srcARC4-03 を以下に示します。

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

//※ main 関数以外は、srcARC4-02 と同一

int main(int argc, const char * argv[])
{
    id mc2 = [[myClass2 alloc] init];
    
    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);
        
        NSLog(@"autoreleasepool 開始\n");
        @autoreleasepool {
            id mc = [mc2 getMyClass1:i];
            [mc showNumber];
            
            //この if 文を追加
            if(i == 2){
                NSLog(@"********ループ強制脱出!");
                break;
            }
        }
        NSLog(@"autoreleasepool 終了\n");
        
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

本来は3周する for ループですが、2周目の時点で、if 文により自動解放プールの内部で break 命令が実行されます。すなわち自動解放プールごと、for ループを強制脱出してしまいます。これを実行すると以下のログが出力されます。

** ループ開始
**** ループ1週目 先頭
autoreleasepool 開始
******** myClass No.1 他クラスのメソッドで生成
******** 私は myClass No.1です。
******** myClass No.1 解放
autoreleasepool 終了
**** ループ1週目 末尾
**** ループ2週目 先頭
autoreleasepool 開始
******** myClass No.2 他クラスのメソッドで生成
******** 私は myClass No.2です。
********ループ強制脱出!
******** myClass No.2 解放
** ループ終了

「myClass No.2」が生成されてから、自動解放プールの終端に達する前に、break 命令によって自動解放プールごとループを強制脱出してしまいます。しかし、だからといって「myClass No.2」が解放されないまま残ったりすることはなく、強制脱出のタイミングできちんと解放されています。自動解放プールを強制脱出しても、それまでに登録されたオブジェクトは問題なく解放されるようです。

なお蛇足ですが、ARC導入以前は以下のように、自動解放プールをオブジェクトとして生成していたそうです。

//ARC導入以前の、旧式な自動解放プール。ARC環境では利用不可。
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
 
[pool release];

この旧式な自動解放プールでは、break 命令などによる強制脱出はしてはいけないそうです。今後これを使うことになる可能性は低そうですが、一応メモ。



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