Objctive-Cにおけるプロパティ属性まとめ。正直、weakとかって使いどころが分かりづらいですよねー

どうもこんばんは、south37です。今日はObjective-Cネタです。

プロパティ属性って何よ!?

さて、皆さん、そもそも プロパティ属性 って分かりますか?Objective-Cでコーディングしてる人ならstongweakcopyなどを目にしていると思いますが、それがいわゆる プロパティ属性 です。

[参考: Objective-Cにおけるプロパティの定義]
http://www.objectivec-iphone.com/introduction/property/property.html

ざっくりと言えば、

@interface SomeClass : NSObject
@property (nonatomic, copy) NSString *name;
@end

という風にクラスのプロパティを宣言する時に、プロパティの性質を宣言する為に指定するものですね。

プロパティ属性の分類

プロパティ属性 は、以下の4種類に分類する事が出来ます。

1. アクセス制御

readonly,readwriteとかで読み取り、読み書きの権限を変更する

@property (nonatomic, strong, readonly) NSArray *nameArray; // readonly指定で読み取りのみ!!

2. 所有属性

strongweakなどオブジェクトの所有権限を指定する事で、オブジェクトの解放のタイミングなんかを決める(適当に指定すると循環参照が出来てメモリリークになったりする)

@property (nonatomic, weak) IBOutlet UILabel *nameLabel; // weak指定でnameLabelへは弱い参照!!

3. アトミックの指定

atomic, nonatomicなどを指定する事で、複数スレッドをたてて処理を並列化した時に、オブジェクトに対する処理を排他的にするかどうか(あるスレッドで処理をしてる時に、別のスレッドからのアクセスをブロックするかどうか)を指定する。大体はnonatomicを指定する事がおすすめされている。

@property (nonatomic, strong) UIWindow *window; // 大抵はnonatomic!!

4. アクセサ名の指定

ゲッタやセッタの名前を自分で決めることができる。

@property (nonatomic, getter=getValue, setter=setValue:) NSInteger hitPoint; // getterとしてgetValue, setterとしてsetValueメソッドを作成!!

プロパティ属性総評

4つの プロパティ属性 の内、1のアクセス制御はまあ別に指定しなくても処理上は問題が無いですし、3のアトミックの指定は大抵nonatomicを指定しておけば良いですし、アクセサ名も変えたければ変えれば良い程度のものです。

しかし、2の 所有属性 についてはメモリリークなどの深刻な問題につながる為、きちんと状況に応じて指定する必要があります。

(ちなみに、上記の記事は古いためか 所有属性 の種類がとても少ないですが、現在はもっと増えてます)

所有属性とはそもそも何なのか

では、そもそも 所有属性 とは何で、どういった種類があるのでしょうか? 簡単にまとめてみると、

所有属性 説明
strong オーナーシップを持つ(参照カウントがincrementされる)
weak オーナーシップを持たない(参照カウントがincrementされない)。オブジェクトが解放されるとnilを返す様になる。
copy コピーに対してオーナーシップを持つ(setterに渡されたオブジェクトのコピーを作るため、元のオブジェクトとは違うオブジェクトを持つ)
unsafe_unretained retain(strongと同じ意味)せずに参照を持つ。weakが無い時代に使われた。今となっては歴史的経緯で残ってるだけのやつで、使っちゃダメ。
assign intやBOOLみたいなプリミティブな値を保持する際に使われる。単純な代入を意味していて、オブジェクトに対して指定すると単なる参照の代入なので要はunsafe_unretainedとかと同じ。これもオブジェクトに対しては使っちゃダメ。
retain strongと同じ。完全に歴史的経緯で残ってるだけなのでやっぱり使っちゃダメ。

の6つがあります。このうち、現在の開発においてオブジェクトに対して指定するのはstrong, weak, copyの3つだけです。この3つの使い分けが若干難しいというか、Objective-Cを勉強し始めの頃はどういうケースで何を指定したらいいのか分からなかったりします。

しょうがないので、とりあえずstrong, weak, copyの役割を順に見ていく事にしましょう。

オブジェクトを保持する場合、デフォルトはstrong

所有属性 を指定する際、デフォルトではstrongを指定する事になります。(何も書かなかったら勝手にstrongになる)

strongは参照カウントをincrementすると書きましたが、要は自明な保持者となる事を意味します。

Objective-Cにおいては、メモリ管理の方法として「参照カウンタ方式」と呼ばれるものを採用しており、自明な保持者が一人でもいればオブジェクトをメモリに残し、一人も居なくなればオブジェクトの解放を行います。実装としてはオブジェクト毎に「参照カウント」という数を定義しておき、自明な保持者が増えるたびに「参照カウント」をincrement、自明な保持者が減れば「参照カウント」をdecrementする事で、参照カウントを見れば残っている保持者数が分かるようになっています。

何だか上手く出来ている気がしますねー。

では、strong以外のweakやcopyを使う場面って何なんでしょうか?何故strongだけではダメなんでしょう?

weak属性の使いどころ

weak属性は、参考書なんかではよく「循環参照を避けるために使われる」とか書かれています。これ、ぶっちゃけ意味分かんないですよね!?そもそも「循環参照」って何やねんって感じですし、具体的なシチュエーションが全く見えてきません。

循環参照って何?

まず、循環参照について理解しておきましょう。「 所有属性 strong」の説明のところで「参照カウンタ方式」について説明しましたが、参照カウンタのdecrementは自身の親のオブジェクトが解放された時にも行われます。すなわち、一つの親オブジェクトから参照されているオブジェクトは、親オブジェクトが解放された時点で一緒に解放されるという訳です。

ところが、 循環参照 という2つ(もしくはそれ以上)のオブジェクトがお互いにstrongで参照を持ち合っている状態が出来てしまう事があります。どちらかが解放される事さえ出来ればもう片方も解放される訳ですが、 1つ以上の参照が残っている限り解放が行われない 為、永遠にメモリ上に残り続けてしまいます。

これは立派なメモリリークですね!!

では、循環参照を避ける為にどうしたらいいの?

Objective-Cにおいては、循環参照を避ける為に明快な解決策が用意されています。それが weak属性を用いる という事です。weak属性を指定すれば参照カウントはincrementされない為、オブジェクトの自明な保持者は片方のオブジェクト(親オブジェクト)だけになり、親オブジェクトが解放された時点で子オブジェクトは解放されるのです。

素晴らしい解決策でしたね!!

っとここまでは色んな文献なんかで述べられてる訳ですが、具体的なweakの使いどころってどこなんでしょう?

よくあるweak属性の使いどころ: 具体例

weak属性が使われるシチュエーションとして、よく例に挙げられるのは次の3つです。

  1. Outletプロパティ
  2. デリゲートプロパティ
  3. ブロックプロパティ内のself

[参考: Objective-Cのプロパティ属性のガイドライン]
http://qiita.com/uasi/items/80660f9aa20afaf671f3#2-2

1から順番に見て行きましょう。

1. Outletプロパティ(iOS開発においては、IBOutletと読み替えてもらえばOK)

Outlet(IBOutlet)が参照するオブジェクトは大抵何かのサブビューです。通常はUILabelなんかをViewControllerからIBOutletで参照する訳ですが、この時UILabelは親のViewからstrongで保持されています。その為、ViewControllerからもstrongで参照してしまうと、親のViewが解放されてもUILabelは解放されない何て事が起きてしまいます(ViewControllerからの参照が残っている為)。

これは望ましい挙動では無い為、 Outletはweakで参照するべき です。

@property (nonatomic, weak) IBOutlet UILabel *nameLabel;

ただし、次にあげるものは例外的にstrongで参照すべきです。

  • Nibのトップレベルオブジェクト(Window, 他のビューに属さないビュー、NSObjectControllerなどビュー以外のオブジェクト)
  • サブビューのうち、-removeFromSuperviewを呼ぶ等して一時的にビュー階層から取り除かれる可能性のあるもの

2. デリゲートプロパティ

あるオブジェクトParentが、別のオブジェクトChildを生成し、Childをstrongで保持するとします。ChildParentに何か処理を委譲したければ、ChilddelegateとしてParentを受け取る必要があります。

// in Parent.h 
@property (nonatomic, strong) Child *child;

// in Parent.m
child.delegate = self;

ここまではよくあるシチュエーションですよね。では、Child.hにおいてdelegateがstrong属性で宣言されている場合はどうなるでしょう!?

// Child.h ダメなケース!!
@property (nonatomic, strong) id delegate;

この場合、ParentオブジェクトはプロパティとしてChildオブジェクトをstrongで持っており、ChildオブジェクトはdelegateプロパティとしてParentをstrongで持っているという循環参照が生まれてしまいます。

メモリリークですよこれは!!

こういったシチュエーションを避けるため、 delegateプロパティにはweakを指定すべき と言われています。

// Child.h Goodなケース!!
@property (nonatomic, weak) id delegate;

[参考: Why use weak pointer for delegation?]
http://stackoverflow.com/questions/8449040/why-use-weak-pointer-for-delegation

3. ブロックプロパティ内のself

ブロックというのは、いわゆる 関数オブジェクト で、メソッドの引数として渡してcallbackの処理を書いたりするのに使われます。JavaScriptにおいて引数として渡すfunction () {}の中にcallbackを書いたりするのと同じ様なイメージです。
(JavaScriptにおいては関数が第一級オブジェクトである為そのまま引数に出来ますが、Objective-Cではそうでは無い為、引数に出来るオブジェクトとしてブロックというものが用意されています。)

プロパティとはちょっと離れるのですが、ブロックにおけるselfへの参照というのも循環参照が発生しやすいポイントとなっています。ブロックは生成されるタイミングと実行されるタイミングが違うため、ブロック内からブロック外のオブジェクトを参照する場合はstrongで保持するようになっています(ブロックの実行中にブロック内から参照されているオブジェクトが解放されるのを防ぐ為)。

その際、ViewControllerの持つプロパティなんかをブロック内から参照すると、そのプロパティの保持者であるViewControllerオブジェクト自身へも強い参照を持つ事になります。ブロックもプロパティとして宣言されていた場合、 ブロックとViewControllerの間で立派な循環参照が出来てしまいますね!!

[参考: ARCでのブロックによるキャプチャと、__weakや引数を使った対処法]
http://i.devris.info/block-arc-capture-weak-argument

// ダメなケース!!

// ParentViewController.h
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^myCallback)(void);

// ParentViewController.m
myCallback = ^{
    NSLog(@"name: %@", self.name); // ブロックからselfへの強い参照が生まれる!!循環参照発生!!
};

こういった事態を避ける為に、一般に ブロックの中でselfを使いたい場合にはselfへの弱い参照を持つ変数を新たに定義する というのがよくあるイディオムとなっています。

// マシなケース

// ParentViewController.m
__weak typeof(self) weakSelf = self; // __weak修飾子をつける事で弱い参照を作る!!
myCallback = ^{
    NSLog(@"name: %@", weakSelf.name); // selfへは弱い参照しか持たないので循環参照にはならない!!
};

上記のコードに出てきた__weakというのはweak修飾子と呼ばれ、弱い参照を持つ変数を生成する為に利用されます。

これで循環参照は回避出来た訳ですが、selfをweakで保持してしまったが為に、weakSelfがnilとなる可能性が出て来てしまいました。そこで、より良い形として次のようなパターンも提案されています。

// いくぶんGoodなケース!!

// ParentViewController.m
__weak typeof(self) weakSelf = self;
mayCallback = ^{
    __strong typeof(self) strongSelf = weakSelf; // ここでweakSelfをstrongSelfで参照。weakSelfがnilならstrongSelfもnil, weakSelfがちゃんとselfを参照していればstrongSelfはselfをstrongで参照する(参照カウントをincrementする)。その為、Bulock内においてstrongSelfが解放される事は無い。
    if (!strongSelf) return; // strongSelfがnilだった場合は処理を中断
     NSLog(@"name: %@", strongSelf.name); 
    [strongSelf doSomething];
} );

[参考: I finally figured out weakSelf and strongSelf]
http://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/

上記のパターンでは、 ブロックの先頭でweakSelfへの強い参照 を作っています。selfが既に解放されていた場合にはweakSelfはnilである為strongSelfもnilになりますが、weakSelfがselfを指している(selfが解放されずに残っている)場合にはstrongSelfはselfへの強い参照を持ちます。その為、ブロック内の処理の実行途中にselfが解放されてしまうといった事態を防ぐ事が出来ます。

尚、この方法ではブロックの実行開始時にselfがnilである可能性を無くす事は出来ないため、最初にstrongSelfのnil判定を行う事が勧められています。nilの可能性を捨てきれないのは悲しいですが、確実に循環参照を防げてかつブロックの実行途中でのオブジェクト解放も起きない為、オススメの方法だと思います!!

[参考: 詳解 Objective-C 2.0 第3版]
http://www.amazon.co.jp/%E8%A9%B3%E8%A7%A3-Objective-C-2-0-%E7%AC%AC3%E7%89%88-%E8%8D%BB%E5%8E%9F/dp/4797368276

補足ですが、weakSelf以外の循環参照を避ける方法として、ブロックへの参照を出来るだけ弱いものにするといった方法も提案されているようです。

[参考: Objective-CのBlocksの循環参照に関する僕なりのベストプラクティス]
http://www.slideshare.net/katty0324/objectivecblocks

循環参照を避ける為に、様々な工夫がなされているみたいですね。

copyの話

最後に、copy属性についても解説しておきます。これは、文字通りオブジェクトをcopyして受け取りたい場合に設定します。

copyを設定すべきシチュエーションとしてよくあるのは、次の2つです。

プロパティの型がMutableなサブクラスを持つ場合(MSString,NSArray,NSDictionary等)

Mutable型な値をsetterに渡されたとき、strongでそれを保持してしまうと意図せず値が変更されてしまう可能性が出てきます。それを防ぐ為に、Mutableなサブクラスを持つ型にはcopy属性をプロパティに指定する事が推奨されています。Immutable型の値が渡された際には浅いコピーが行われる(参照のみが渡される)為、パフォーマンスの悪化は置きません。

@property (nonatomic, copy) NSString *name;

ブロック型のオブジェクトをプロパティにする場合

ブロックオブジェクトは生成時にメモリのスタック領域に実体を確保されるのですが、スタック領域に確保されたオブジェクトはそのスコープの終了時に解放されてしまいます。ブロックオブジェクトを生成されたスコープの外(メソッドや関数の外)でも使えるようにする為には、ブロックオブジェクトをコピーしてヒープ領域に複製を作ってやる必要があります。

ただし、iOS開発時などはARC(Automatic Reference Counting)がオンになっている為、 強い参照の変数へ代入される場合returnの返り値として返される場合 にはコンパイラが自動的にcopy操作のコードを挿入してくれます。

// myClass.h
@interface myClass : NSObject {
    int (^myBlock)(int);
}

// myClass.m
// 強い参照の変数myBlockへブロックオブジェクトが代入される。この時、実際にはcopyされてヒープに確保されたブロックオブジェクトがmyBlockの参照先となる。
myBlock = ^int (int i) { 
    return i;
};

一方、メソッドや関数の引数として渡されたブロックは自動ではcopyされません。 setterの引数として渡されたオブジェクトがcopyされるようにする為には、copy属性をプロパティの宣言時に指定してやる必要があります。

@property (nonatomic, copy) int (^myBlock)(int);

copy属性が指定された事でsetterにおいてcopyメソッドの呼び出しが行われ、ヒープ領域に確保されたオブジェクトがプロパティとして保持される事になります。

// setterはこんなイメージのものが自動で定義される。自分で実装する訳ではない。
- (void)setMyBlock:(int (^) (int i))myBlock
{
    if (_myBlock != myBlock) {
        [_myBlock release];
        _myBlock = [myBlock copy];
    }
}

これで、問題無くブロックオブジェクトをプロパティとして宣言出来る様になりました!!

まとめ

今回はObjective-Cにおけるややこしい プロパティ属性 の役割や、具体例を知らなければイメージしにくいweakcopyの利用シーンについてまとめてみました。初学者にとっては分かりづらく、どうしても後回しにしたくなる部分ではありますが、ある程度の規模のアプリケーションを作る上では必須になってくる知識でもあります。メモリリークやクラッシュの可能性を限りなく小さくする為にも、こういった事をきちんと学んでいきたいですね。