工場裏のアーカイブス

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

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

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

弱い参照について

実験5-01

これまで述べてきたように、ARCでは変数(id 型など)がオブジェクトを参照する場合、デフォルトで強参照として扱われます。また、以下のように「__strong」という修飾子を変数に付けることによって、明示的に強参照を指定することが可能です。

//変数 mc からのオブジェクトへの参照は、強参照となる。
__strong id mc = [[myClass1 alloc] init];

//__strong 修飾子は、以下のような位置に付けても可
id __strong mc = [[myClass1 alloc] init];

実は、ARCには「弱い参照(弱参照)」という概念も存在します。これは以下のように「__weak」という修飾子を変数に付けて指定します。

//変数 mc からのオブジェクトへの参照は、弱参照となる。
__weak id mc = [[myClass1 alloc] init];

//先程同様に、修飾子は以下のような位置に付けても可
id __weak mc = [[myClass1 alloc] init];

例えば、ある変数 A がオブジェクトを弱参照しても、そのオブジェクトは保持されません(A はオーナーにはなりません)。従って、弱参照をする変数に代入されるオブジェクトは、オーナーが別に存在している必要があります。そしてそのオーナーが居なくなると、A からの弱参照が存在していてもオブジェクトは解放されてしまいます。このとき、A には自動的に nil が代入されます。詳しくは後述しますが、この nil が代入されるという点は地味に重要です。

以下では強参照と弱参照について具体的に実験してみます。まず基本となるプログラム(srcARC5-01)を以下に示します。

//srcARC5-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[])
{
    __strong id mc1;
    __strong id mc2;
    
    mc1 = [[myClass1 alloc] init];
    mc2 = mc1;
    mc1 = nil;
    NSLog(@"mc1 にnilを代入");
    
    [mc2 showNumber];
    
    return 0;
 }

srcARC1-01 に少し手を加えただけの、非常に単純なプログラムです。変数 mc1、mc2 の __strong 修飾子は無くても構いません(ARCでは、変数からオブジェクトへの参照は、デフォルトで強参照となるため)。これを実行すると以下のログが出力されます。

mc1 にnilを代入
******** 私は myClass No.0です。
******** myClass No.0 解放

このプログラム自体は、何も目新しいことはしていません。mc1 が強参照している myClass1 のオブジェクトをmc2 に代入し、その後 mc1 に nil を代入しています。ここで mc1 からオブジェクトへの強参照は外れますが、代入により mc2 もオブジェクトを強参照するようになったので、ここではオブジェクトは解放されません。mc2 にメッセージを送信して、メソッドを呼び出すことも当然可能です。


実験5-02

それでは、srcARC5-01 における変数 mc2 の修飾子を __weak に変更してみます(以下の srcARC5-02)。 それ以外の変更は一切加えません。

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

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

int main(int argc, const char * argv[])
{
    __strong id mc1;
    __weak id mc2;
    
    mc1 = [[myClass1 alloc] init];
    mc2 = mc1;
    mc1 = nil;
    NSLog(@"mc1 にnilを代入");
    
    [mc2 showNumber];
    
    return 0;
 }

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

******** myClass No.0 解放
mc1 にnilを代入

今度は mc2 にオブジェクトを代入しても、mc2 はオブジェクトを弱参照するためオーナーにはなりません。そのため mc1 に nil を代入した時点で、オブジェクトは解放されてしまいます。このとき mc2 には前述のように、自動的に nil が代入されます。

その後、以下のように mc2 にメッセージを送信して、メソッドを呼び出そうとしているわけですが、mc2 には既に nil が代入されています。従ってここでは nil に対してメッセージを送信しているわけです。

[mc2 showNumber];

一見エラーが発生しそうですが、実は Objective-C では nil へどんなメッセージを送信をしても「何もしない」という仕様となっているそうです。従って、この行では何の処理も行われず、何事も無かったかのようにプログラムは進行します。このため、例えば以下のような書き方でエラー対策をする必要は全くありません。これが弱参照の地味ですが重要なポイントの一つです。

//こんなエラー対策は不要!
if(mc2 != nil){
    [mc2 showNumber];
}

 

循環参照について

実験5-03

ARCには弱参照という概念があるのは分かりましたが、どのようなときに必要になるのでしょうか。弱参照の主な用途として「循環参照」の回避が挙げられます。循環参照とは例えば、あるオブジェクト A のインスタンス変数がオブジェクト B を強参照しており、また B のインスタンス変数も A を強参照しているような状態です。 A と B はお互いがお互いのオーナーとなっているため、どちらも解放されることが無くなってしまいます。以下の srcARC5-03 で、循環参照を引き起こす具体的なプログラム例を示します。

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

//myClassA の定義内で、まだ定義されていない myClassB を用いるための前方宣言
@class myClassB;

@interface myClassA : NSObject{

}
@property(nonatomic, strong) myClassB *B;

@end

@implementation myClassA


-(void)showMe{
    NSLog(@"私は myClassA です。\n");
}

-(void)dealloc
{
    NSLog(@"myClassA 解放\n");
}

@end


@interface myClassB : NSObject{
    
}
@property(nonatomic, strong) myClassA *A;

@end

@implementation myClassB

-(void)showMe{
    NSLog(@"私は myClassB です。\n");
}

-(void)dealloc
{
    NSLog(@"myClassB 解放\n");
}

@end


int main(int argc, const char * argv[])
{
    @autoreleasepool {
        myClassA *mca = [[myClassA alloc] init];
        myClassB *mcb = [[myClassB alloc] init];
        
        //循環参照を引き起こす!
        mca.B = mcb;
        mcb.A = mca;
        
        [mca.B showMe];
        [mcb.A showMe];
    }
    
    return 0;
}

ここでは2つのクラス myClassA、myClassB を定義しています。これらのクラスはそれぞれ、相手方のクラスを強参照するインスタンス変数(プロパティを介して)を有しています。ここではプロパティを用いていますが、強参照、弱参照の考え方は普通の変数と何ら変わりません。以下のように strong、weak という属性を付けることによって、参照のタイプを制御することが出来ます。

//強参照を持つプロパティの宣言例
@property(strong, nonatomic) myClass1 *mc;

//弱参照を持つプロパティの宣言例
@property(weak, nonatomic) myClass1 *mc;

そして main 関数では両者のオブジェクトを生成し、プロパティにお互いを代入させ合って循環参照を引き起こさせています。これを実行すると以下のログが出力されます。

私は myClassB です。
私は myClassA です。

見てのとおり、両方のオブジェクトについて dealloc メソッドが呼び出されていません。循環参照によって、両方のオブジェクトが解放されないまま残り続けてしまっているわけです。これはメモリリークの原因となります。


実験5-04

循環参照が起こるのを回避するためには、弱参照の概念が必要となります。と言っても難しいことではなく、srcARC5-03 の場合は myClassA か myClassB のどちらかについて、プロパティの属性を弱参照に変更するだけです。ここでは以下のように、myClassA の持つプロパティの方を変更してみます。

//myClassA の持つプロパティの属性を弱参照に変更
//(myClassB の持つプロパティの方を変更してもよい)
@property(nonatomic, weak) myClassB *B;

プロパティ属性を変更した srcARC5-03 を実行すると、以下のログが出力されます。

私は myClassB です。
私は myClassA です。
myClassB 解放
myClassA 解放

今度はきちんと、両方のオブジェクトが解放されています。myClassA のオブジェクト(のインスタンス変数)は myClassB のオブジェクトを弱参照しているため、myClassB のオブジェクトのオーナーは変数 mcb だけとなります。これによって、循環参照は解消されます。


弱参照と自動解放プール

実験5-05

ところで srcARC5-03 では、main 関数内で自動解放プールを用いていますが、一見これは必要なさそうに見えます。実際、素の srcARC5-02 では自動解放プールを消去しても、全く同じ動作をします(循環参照が起こります)。

しかし、実験5-04 のように弱参照を用いたときは話が違います。自動解放プールを消去した状態で実験5-04 の内容を実行すると、出力されるログは以下のようになります。

私は myClassB です。
私は myClassA です。

なんと、きちんと弱参照を用いて循環参照を解消したはずなのに、両方のオブジェクトが解放されなくなってしまいました。非メソッドファミリのメソッドによるオブジェクト生成などを特に行っていないにも関わらず、自動解放プール無しではプログラムが意図した通りに動作しません。もう少し検証してみたいと思います。


実験5-06

弱参照と自動解放プールの関係を検証するために、別の実験をしてみます。まずプログラム(srcARC5-06)を以下に示します。

//srcARC5-06
//当然ながら、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(@"autoreleasepool 開始\n");
    @autoreleasepool{
        
        NSLog(@"** ローカルスコープ開始\n");
        {
            __strong id smc = [[myClass1 alloc] init];
            __weak id wmc;
            
            wmc = smc;
            [wmc showNumber];
            
        }
        NSLog(@"** ローカルスコープ終了\n");
        
    }
    NSLog(@"autoreleasepool 終了\n");
    
    return 0;
}

myClass1 クラスについては srcARC5-01 と全く同じです。そして main 関数では、まず自動解放プールを用意し、その内側にローカルスコープを用意しています。そしてローカルスコープ内で alloc/init メソッドにより myClass1 のオブジェクトを生成して強参照の変数 smc に代入しています。また直後に、弱参照の変数 wmc にも代入しています。

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

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

smc はローカルスコープ内のローカル変数なので、ローカルスコープを抜けるときに当然消滅します。wmc はオブジェクトを弱参照しかしていないので、このタイミングでオブジェクトのオーナーが居なくなり解放される……かと思いきや解放されていません。そして、自動解放プールが終了するタイミングでオブジェクトが解放されています。

つまり、オブジェクトはいつの間にか自動解放プールに登録されているようです。おそらくARCでは、弱参照の変数にオブジェクトを代入すると、そのとき同時にオブジェクトが自動解放プールに登録されるのではないか、と最初は考えました。

しかし……例えば srcARC5-02 では自動解放プールを用いていませんが、弱参照の変数に代入したオブジェクトは問題なく解放されています。上記の考え方が正しいならば、 srcARC5-02 ではARCについて色々実験(3)で述べたような、自動解放プールに登録されるべきオブジェクトが登録されないことによるメモリリークが起こってしまうはずです。従って、オブジェクトが自動解放プールに登録されるためには別の条件が存在するようです。


実験5-07

ここで、srcARC5-06 の main 関数内にある [wmc showNumber]; の行だけを以下のようにコメントアウトして実行してみます。それ以外の変更は一切加えません。

//[wmc showNumber];

showNumber メソッド(ここでは弱参照の変数 wmc を介して呼び出している)は、ただ NSLog 関数を用いて文章を出力するだけのメソッドです。これをコメントアウトした所で、オブジェクトの状態には何ら影響を及ぼさないように思えてしまいます。しかし、出力されるログを見ると……

autoreleasepool 開始
** ローカルスコープ開始
******** myClass No.0 解放
** ローカルスコープ終了
autoreleasepool 終了

なんと実験5-06 と異なり、ローカルスコープを抜けるタイミングでオブジェクトが解放されています。オブジェクトは自動解放プールには登録されておらず、smc からの強参照が外れてオーナーが居なくなるタイミングで素直に解放されるようです。

文章出力のみを行う showNumber メソッドをコメントアウトしただけで、オブジェクトが自動解放プールに登録されるか否かの違いが生じてしまいました。どうやら、メソッドの動作内容よりも、メソッドを呼び出すこと自体がカギとなっていそうです。


これらの結果から、ARCにおける弱参照の変数へのオブジェクトの代入は、以下のように処理されると推測されます。

  • 弱参照の変数にオブジェクトを代入しても、その時点ではオブジェクトは自動解放プールに登録されない。
  • 弱参照の変数を介して、オブジェクトに何らかのメッセージを送信する(メソッドを呼び出す)と、同時にそのオブジェクトが自動解放プールに登録される。

あくまでも推測であり、本当にこのような処理となっているのか断言までは出来ません。しかし、このように考えれば、srcARC5-02 で自動解放プールが無くても問題がない理由も説明が付きます(弱参照の変数を介したオブジェクトへのメッセージ送信が行われる前に、変数 mc1 への nil 代入によってオブジェクトが解放されてしまうため)。

ただし実用的なプログラムでは、弱参照の変数を用いるときに、オブジェクトを代入するだけでメッセージ送信を一切しない、などということはあまり無いでしょう。従って、弱参照の変数を利用する際は、必ず自動解放プールを用意した方が間違いが無いと思います。



(ひとまず一区切り。今後何か新しいことが分かったら、続きを書くかもしれません。)

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) に続きます。

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) に続きます。

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

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

自作のイニシャライザによるオブジェクトの生成

実験2-01

srcARC1-01 の myClass1 は num というインスタンス変数を持っていますが、この値を設定する方法が用意されていませんでした。そこで、この myClass1 に引数を持つイニシャライザを追加して、引数により num の値を設定出来るようにしてみます。また、それに合わせて main 関数の中身も for ループを用いたものに書き換えます。それを srcARC2-01 として以下に示します。

//srcARC2-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;
}

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

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

@end


int main(int argc, const char * argv[])
{
    //for ループを用いたものに書き換え

    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);
            
        id mc = [[myClass1 alloc] initWithNumber:i];
        [mc showNumber];
            
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

イニシャライザであるinitWithNumber: メソッドは引数 n を持ち、この値によってインスタンス変数 num を初期化出来ます。なお、イニシャライザを定義するときには

self = [super init];

のように、 self にスーパークラスのイニシャライザの戻り値を代入する書き方が定石となっています。しかしARC利用時には、init メソッドファミリに属するメソッド以外のメソッドで、self にオブジェクトを代入するとエラーが出るようです(initWithNumber: は init メソッドファミリであるためOK)。 つまりイニシャライザは、ARCでは必ず init メソッドファミリである必要があるようです。

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

** ループ開始
**** ループ1週目 先頭
******** myClass No.1 イニシャライザにより初期化
******** 私は myClass No.1です。
**** ループ1週目 末尾
******** myClass No.1 解放
**** ループ2週目 先頭
******** myClass No.2 イニシャライザにより初期化
******** 私は myClass No.2です。
**** ループ2週目 末尾
******** myClass No.2 解放
**** ループ3週目 先頭
******** myClass No.3 イニシャライザにより初期化
******** 私は myClass No.3です。
**** ループ3週目 末尾
******** myClass No.3 解放
** ループ終了

initWithNumber: はメソッドファミリに属するため、これ(と alloc)により生成されたオブジェクトを変数 mc (デフォルトで強参照)に代入することによって、mc はオブジェクトのオーナーになります。そして mc は for ループ内のローカル変数であるため、ループが回るたびにその末尾で消滅し、ここで mc からの強参照が外れたオブジェクトの dealloc が呼び出されて解放されています。


実験2-02

次に、srcARC2-01 の main 関数を以下の srcARC2-02 のように書き換えて、変数 mc の宣言を for ループの外側に持ってきます。

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

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

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

    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);

        mc = [[myClass1 alloc] initWithNumber:i]; 
        [mc showNumber];
            
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

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

** ループ開始
**** ループ1週目 先頭
******** myClass No.1 イニシャライザにより初期化
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 イニシャライザにより初期化
******** myClass No.1 解放
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 イニシャライザにより初期化
******** myClass No.2 解放
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放

今度は変数 mc は、for ループに関係なく main 関数の最後まで存在し続けます。そのため srcARC2-01 とは違い、ループの末尾でオブジェクトの解放が起こることはありません。そして次回のループの冒頭で、新しいオブジェクト(例えば「myClass No.2」)が生成されて mc に代入されると、当然古いオブジェクト(例えば「myClass No.1」)への強参照が外れます。このとき、この古いオブジェクトの dealloc が呼び出されて解放されています。

また、最後の回のループ(3週目)で生成される「myClass No.3」が mc に代入されると、それ以降 mc には代入が起こりません。そのため「myClass No.3」は mc に保持されたまま main 関数の最後まで存在し続けます。


init 以外のメソッドファミリに属するメソッドによるオブジェクトの生成

実験2-03

実は、「詳解 Objective-C 2.0 第3版」によると、メソッドファミリの中でも init とそれ以外(alloc、copy、mutableCopy、new)のメソッドファミリでは、若干扱いが異なるようです。

init メソッドファミリについては、書籍中で以下のように説明されています。

init メソッドファミリはインスタンスメソッドでなければならず、Objective-C のポインタタイプを返さなければならない。返り値は id 型か、宣言しているクラスのスーパークラスかサブクラスへのポインタでなければならない。

一方、それ以外の全てのメソッドファミリについては、以下のように説明されています。クラスメソッドとインスタンスメソッドの区別も無いそうです。

メソッドは保持可能なオブジェクトへのポインタを返さなくてはならない。

これによると、やはりARCでは init メソッドファミリに属するメソッドは、イニシャライザとして用いることが前提となっているようです。そもそもObjective-Cでは、ARCが導入されるもっと以前から、イニシャライザの名前は init または init で始まる文字列とするのが慣例であったそうです。その慣例が init メソッドファミリという形で、文法的なルールとして取り入れられたという所でしょうか(あくまで自分なりの理解ですので、間違っているかもしれませんが)。

一方で、それ以外のメソッドファミリには init ほどの制約は無いようです。そこで、srcARC2-01 の myClass1 に、以下の srcARC2-03 のように newMyClassWithNumber: というクラスメソッド(これは new メソッドファミリに属します)を追加してみます。main 関数の内容も、このメソッドを用いるように少しだけ書き換えてあります。

//srcARC2-03
//当然ながら、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;
}

//このクラスメソッドを追加
+(id)newMyClassWithNumber:(int)n{
    NSLog(@"******** myClass No.%i newMyClassWithNumber:で生成\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(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);
            
        id mc = [myClass1 newMyClassWithNumber:i];
        [mc showNumber];
            
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

newMyClassWithNumber: メソッドでは、以下のように alloc と initWithNumber: によってオブジェクトを生成し、それを戻り値として返します(このメソッドはクラスメソッドなので、ここでの self は myClass1 というクラス自体を指すことに注意が必要です)。

return [[self alloc] initWithNumber:n];

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

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

「〜newMyClassWithNumber:で生成」と文章が書き変わっただけで、動作内容自体は srcARC2-01 と全く同じです。つまり、以下のように alloc と initWithNumber: メソッドを用いたオブジェクト生成手順を、ひとつのメソッドにまとめたことになります。

//以下の2つのコードは、同じ動作をする
id mc = [[myClass1 alloc] initWithNumber:i];

id mc = [myClass1 newMyClassWithNumber:i];

なお newMyClassWithNumber: メソッドは、例えば「copyMyClass〜」や「allocMyClass〜」という具合に、他のメソッドファミリに属するように名前を書き換えても、全く同様に動作してしまうようです(ただし「initMyClass〜」といった名前に書き換えると、メソッドファミリに属していると認識されなくなります。前述の 「init メソッドファミリはインスタンスメソッドでなくてはならない」という規則に反するからのようです)。

しかし、だからといってこのメソッドに「copyMyClass〜」や「allocMyClass〜」なんて名前を付けるべきでは無いでしょう。名前から予想されるメソッドの動作と、実際の動作が乖離してしまいます。メソッドファミリの各キーワードは、プログラマが意識的に、それぞれの名前に応じた適切な使い分けをすることが必要だと思います。


ここまでの実験では、メソッドファミリに属するメソッドが、戻り値としてオブジェクトを返す場合について検証してきました。それでは、メソッドファミリに属さないメソッドについてはどうでしょうか?例えば、newMyClassWithNumber: メソッドを、メソッドファミリに属さない名前に書き換えたら何が起こるのでしょうか?

それについては、ARCについて色々実験(3) 以降で実験をして行きます。

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 を勉強する上で、非常に参考となる解説書です

iPhoneアプリ開発環境の変遷についてのメモ

 iPhoneアプリの開発環境にはしばしば、主にXcodeのバージョンアップなどに伴う大規模な変動が生じるようです(iPhone本体の新世代機種リリースや、iOSのバージョンアップなどがあったときは特に)。

 Xcodeの画面レイアウトが変更されたり機能が追加されたりするだけならいざ知らず、自動生成されるテンプレートのソースコードが全く別物になっていたり、新しいiOSではいくつかの関数が使えなくなることがあったりするようです(iOS 6に対応したアプリを作ろうとしたときに、実体験しました)。
 特に、一番最初の頃は「それがいつの時点の情報であるのか、どのバージョンのiOSやXcodeに対応した情報であるのか」ということを全く意識出来ずに、闇雲にネットや解説書で情報を集めていたので、かえって混乱してしまっていた記憶があります(特に、Xcode 4.2でARCが登場する前後のソースコードの違いには泣かされました)。

 そこで、アプリ開発環境に大きな変化を及ぼしたと考えられる出来事の日付についてメモしてみます(今後、追記する可能性も)。情報収集をする上での目安として。

日付 出来事 備考
2010/06/21 iOS 4.0 リリース  
2011/03/09 XCode 4.0 リリース Xcode IDEへのInterface Builderの統合
2011/10/12 iOS 5.0 リリース  
2011/10/12 XCode 4.2 リリース Storyboard機能追加、ARC機能追加
2012/09/19 iOS 6.0 リリース  
2012/09/19 XCode 4.5 リリース iOS 6.0向けアプリの開発に対応
2012/09/21 iPhone 5 発売 画面サイズが3.5インチから4インチに変更