工場裏のアーカイブス

素人による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 代入によってオブジェクトが解放されてしまうため)。

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



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