工場裏のアーカイブス

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

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

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

メソッドファミリに属さないメソッドによるオブジェクトの生成(コンビニエンスコンストラクタ

実験3-01

これまでの実験では、メソッドファミリに属するメソッドの戻り値として、オブジェクトを返す場合について様々な検証をしてきました。そこで今度は、メソッドファミリに属さないメソッドの戻り値として、オブジェクトを返す場合について検証したいと思います。

srcARC2-03 の newMyClassWithNumber: メソッドを、いかなるメソッドファミリにも属さない myClassWithNumber: という名前に書き換えてみます。また、main 関数の中身を自動解放プール(@autoreleasepool)を追加したものに書き換えます。これを srcARC 3-01 として以下に示します。

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

#import <Foundation/Foundation.h>

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

@implementation myClass1

-(id)initWithNumber:(int)n{
    self = [super init];
    if(self != nil){
        num = n;
        //NSLog(@"******** myClass No.%i イニシャライザにより初期化\n", n);
    }
    
    return self;
}

//srcARC2-03 の newMyClassWithNumber: メソッドを書き換え
+(id)myClassWithNumber:(int)n{
    NSLog(@"******** myClass No.%i myClassWithNumber: で生成\n", n);
    return [[self alloc] initWithNumber:n];
}

-(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(@"autoreleasepool 開始\n");
    @autoreleasepool {
        NSLog(@"** ループ開始");
        for(int i = 1; i <= 3; i++){
            NSLog(@"**** ループ%i週目 先頭\n", i);
            
            id mc = [myClass1 myClassWithNumber:i];
            [mc showNumber];
            
            NSLog(@"**** ループ%i週目 末尾\n", i);
        }
        NSLog(@"** ループ終了\n");
    }
    NSLog(@"autoreleasepool 終了\n");
    
    return 0;
}

newMyClassWithNumber: メソッドと、myClassWithNumber: メソッドは、名前(とNSLog 関数で出力する文字列)を変えただけです。動作内容には手を加えていません。両者とも以下の全く同じコードによって myClass1 のオブジェクトを生成し、戻り値として返しています。

return [[self alloc] initWithNumber:n];

両者の違いは、ただ名前がメソッドファミリに属しているか否かだけです。

これを実行すると、以下のログが出力されます。

autoreleasepool 開始
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 myClassWithNumber: で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 myClassWithNumber: で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 myClassWithNumber: で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放
******** myClass No.2 解放
******** myClass No.1 解放
autoreleasepool 終了

ログを見ると、ループが1周するたびに新しいオブジェクトが生成されていますが、ループの内部ではオブジェクトの解放は一切起こっておりません。ループを抜けた後に、自動解放プールを抜けるタイミングで、生成された3つのオブジェクトがまとめて解放されています。srcARC2-03 の実行結果とは明らかに様子が異なっています。

前述のようにARCでは、メソッドファミリに属さないメソッドの戻り値として返されるオブジェクトは、参照カウンタ方式で言うところの autorelease 操作が自動的に施されます。myClassWithNumber: で戻り値を返すコードは、ARCでは自動的に、参照カウンタ方式における以下のようなコードと同様に扱われるようです。

//※参照カウンタ方式(ARCオフ)におけるコード
return [[[self alloc] initWithNumber:n] autorelease];

すなわち、myClassWithNumber: の戻り値として得られるオブジェクトは、自動解放プールに登録されます。

myClassWithNumber: メソッドの戻り値であるオブジェクトを、変数 mc に代入すると、これまでと同様に mc はオブジェクトを強参照しますが、同時にオブジェクトは、ループ全体を囲む自動解放プールに登録されます。mc は for ループ内のローカル変数であるため、ループが1周するたびに消滅してオブジェクトへの強参照が外れるのですが、自動解放プールが存在し続けているため、ここでオブジェクトの解放は起こりません。

そしてループが1周するたびに、新しいオブジェクトが自動解放プールに登録されていき、最終的にループを抜けた時点では、3つのオブジェクトが自動解放プールに登録されています。そして自動解放プールを抜けるタイミングで、登録されていた3つのオブジェクトがまとめて解放されます。ARCではオーナーがいなくなったオブジェクトであっても、自動解放プールに登録されている限りは保持され続けるようです。


実験3-02

それでは、もし自動解放プールを用意せずに、myClassWithNumber: を用いたらどうなるでしょうか。srcARC3-01 の main 関数にある自動解放プールをコメントアウトして、srcARC3-02 のように書き換えてみます。

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

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

int main(int argc, const char * argv[])
{
    //自動解放プールをコメントアウト
    //NSLog(@"autoreleasepool 開始\n");
    //@autoreleasepool {
        NSLog(@"** ループ開始");
        for(int i = 1; i <= 3; i++){
            NSLog(@"**** ループ%i週目 先頭\n", i);
            
            id mc = [myClass1 myClassWithNumber:i];
            [mc showNumber];
            
            NSLog(@"**** ループ%i週目 末尾\n", i);
        }
        NSLog(@"** ループ終了\n");
    //}
    //NSLog(@"autoreleasepool 終了\n");
    
    return 0;
}

これを実行すると、以下のログが出力されます。

** ループ開始
**** ループ1週目 先頭
******** myClass No.1 myClassWithNumber: で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 myClassWithNumber: で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 myClassWithNumber: で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了

ログを見ると、それぞれのオブジェクトは生成はされており、オブジェクトのメソッド(shouNumber メソッド)も利用は出来るのですが、オブジェクトの解放が全く行われていません。すなわち、メモリリークが起こってしまっています。

あくまでこれは想像なのですが、ARCでは、メソッドファミリに属さないメソッドの戻り値として得られた直後のオブジェクトは、自動解放プールに登録されることを前提として、内部的に retainCount の値が 1 になっているのだと思います。そしてこのオブジェクトが mc に代入されると、mc からの強参照によって retainCount の値が 2 に増えます。

そして、自動解放プールがきちんと用意されている場合には、mc からの強参照が外れるときにオブジェクトの retainCount が減って 1 となり、自動解放プールを抜けるときにまた retainCount が減って 0 となり、ここでオブジェクトが解放されます。

しかしながら、自動解放プールが用意されていない場合には、mc からの強参照が外れたときにオブジェクトの retainCount が減って 1 となりますが、それ以上 retainCount を減らすものが存在しなくなってしまいます。そのため、オブジェクトは解放されずにいつまでも残り続けてしまうのだと思います(繰り返しますが、あくまで内部的な動作の想像です。実際に確かめたわけではないので鵜呑みにはしないでください)。

したがってARCでは、メソッドファミリに属さないメソッドの戻り値としてオブジェクトを受け取る場合には、自動解放プールを用意することは必須となるようです。


実験3-03

それでは、srcARC3-01 を以下の srcARC3-03 のように書き換えてみます。

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

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

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

ここでは、ループおよび自動解放プールの外側で変数 mc2 を宣言しています。そして、ループが i = 2 のときに生成されるオブジェクトを、mc2 = mc のように代入しています。そして自動解放プールを抜けた後で、mc2のメソッド(showNumber)を呼び出しています。これを実行すると以下のログが出力されます。

autoreleasepool 開始
** ループ開始
**** ループ1週目 先頭
******** myClass No.1 myClassWithNumber: で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 myClassWithNumber: で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 myClassWithNumber: で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放
******** myClass No.1 解放
autoreleasepool 終了
******** 私は myClass No.2です。
******** myClass No.2 解放

ログを見ると、「myClass No.1」および「myClass No.3」は srcARC3-01 と同様に、自動解放プールを抜けるときにまとめて解放されています。一方で、ループ内の以下のコードによって「myClass No.2」は自動解放プールの外側の mc2 に代入されます。

if(i == 2) mc2 = mc;

そのため「myClass No.2」は自動解放プールを抜けても、mc2 に強参照をされているため解放されず、showNumber メソッドもきちんと利用できて「私は myClass No.2です。」というログを出力します。そして main 関数が終了し、mc2 が消滅するタイミングで「myClass No.2」は解放されるようです。

したがって、自動解放プールに登録されたオブジェクトであっても、その外側で宣言された(強参照の)変数に代入すれば、自動解放プールを抜けた後でもその変数がオーナーとなってオブジェクトを保持することが可能であるようです。


なお、この myClassWithNumber: メソッドのように、alloc + init(または init〜 という名前のイニシャライザ)でオブジェクトを生成し、それを自動解放プールに登録される状態(参照カウンタ方式で言うと autorelease メソッドが呼び出された状態)で戻り値として返してくれるクラスメソッドを「コンビニエンスコンストラクタ」と言います。

例えば、Foundationフレームワークのクラス(NSString など)にもコンビニエンスコンストラクタ(stringWithString: など)を有するものが多数ありますが、これらを利用するときには、やはり自動解放プールを用意することが必須であると考えた方が良さそうです。



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