blocksには数多くの落とし穴があります。
しかしそれを乗り越えることができれば、非常にスマートにかける状況はそれなりに増えると思います。
※注意:ここではARC環境での話です。一部MRCと状況が異なる場合があるかもしれません。
<blocksはObjective-cのオブジェクトをstrong参照でキャプチャする>
ここでいうキャプチャとは、ポインタをコピーしている、ということです。
なんで?と思われる方も多いと思います。しかし理由ははっきりしていています。
例えば、dispatch_afterで処理を遅延することを考えてみましょう。
NSArray *sameArray = ...;
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
id obj = sameArray[2];
});
ここでdispatch_afterに渡されたblocksは2秒後に遅延実行されます。
さて、sameArrayはARC管理オブジェクトですね。ということは、何もしなければ、2秒後には解放されてしまっているオブジェクトなんですね。しかし実際にはblocksがキャプチャ、つまりstrong参照を保持しているために、BAD_ACCESSにはならないんです。
blocksのキャプチャというのは非常に理にかなっているわけです。
さて、ここでさらに注意しなければならないことがあります。
<blocksがキャプチャするのはObjective-cのオブジェクトだけ>
例えばCGImageRefなんてどうでしょうか? そう、もちろんキャプチャしません。
つまり、先ほどのように遅延実行で、CGImageRefは参照こそできるが、strongではもてないんです。ということは簡単にBAD_ACCESSがおこります。つまり、自分でCGImageRetainしないといけないんですね。これは本当に注意が必要です。
<blocksの中でメンバ変数を使用すると、selfをstrongキャプチャする>
これも恐ろしいです。仮にblocksをメンバにもつクラスがあり、そのblocksがselfをキャプチャすると、簡単に循環参照でメモリリークです。
@interface Hoge : NSObject
@property (nonatomic, copy) void (^blocks)();
@end
@implementation Hoge
{
NSNumber *value;
}
- (id)init
{
if(self = [super init])
{
value = @10;
self.blocks = ^{
[value description]; //循環参照!
};
}
return self;
}
- (void)dealloc
{
printf("dealloc");
}
@end
これをなんとかするには
__block __weak Hoge *_self = self;
などと一時変数としてweak参照の変数を使ったりすることで解決できますが、冗長でスマートではなく、私はあまり好きではありません。
<blocksの寿命は記述したスコープのみ>
厳密にはblocksによってはスタック領域に確保されるのではないblocksもあるのですが、
このように考えていた方が統一的で安全です。
void (^blocks)();
{
blocks = ^{};//寿命はこのスコープのみ!
}
//ここでは既に死んでいる
blocks();
このようなことをする場合は次のようにするのを推奨します
void (^blocks)();
{
blocks = [^{} copy];//スタックからヒープに移動
}
//ここではヒープにあるblocksを参照、またARCで自動破棄
blocks();
copyすると、blocksはスタックメモリからヒープメモリへと移動できるので、安全に後から呼べるのです。
関数の戻り値としてblocksを返す場合(関数言語でいうところの高階関数)も同様に、
copyしておくことを推奨します。
また、クラスプロパティもcopy属性推奨です。
つまり、「スコープを出た後でも呼び出したいblocksはコピーする」というのが安全の指標となるでしょう。dispatch_afterなんかは、内部で受けとったblocksをコピーしているので、呼び出し側では特に気にせずつかえるのです。
<blocksはObjective-cオブジェクトである>
先の例からも分かる通りです。しかしまあARC下では参照が自然と消滅するため、それほど意識しなくても良いかもしれません(循環参照以外では)。
<blocksはキャプチャした変数をconst コピーする>
int a = 0;
void (^blocks)() = ^{
a = 3; //constなコピーなのでだめ!
};
こういう場合に__blockを使います
__block int a = 0;
void (^blocks)() = ^{
a = 3; //ok!
};
また、次のような使い方すら可能です。
typedef int (^Hoge)();
Hoge function()
{
__block int n = 10;
return [^{
return n--;
} copy];
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
Hoge hoge = function();
for(int i = 0 ; i < 5 ; ++i)
{
printf("%d, ", hoge()); //10, 9, 8, 7, 6,
}
}
return 0;
}
__blockの変数はcopyされるとスタックからヒープに本体と一緒に移動できるんです。
じゃあ次の例ではどうでしょうか?
typedef int (^Hoge)();
Hoge function()
{
__block int n = 10;
return [^{
return n--;
} copy];
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
Hoge hoge1 = function();
Hoge hoge2 = [hoge1 copy];
for(int i = 0 ; i < 5 ; ++i)
{
printf("%d, ", hoge1());
}
for(int i = 0 ; i < 5 ; ++i)
{
printf("%d, ", hoge2());
}
//10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
}
return 0;
}
そう、__blockのついた変数はコピーしても共有されるんですね。面白いです。
ここまで読むと、blocksは主に循環参照で非常に欠陥が多く、弱点もおおいのです。
それも仕方の無いことで、ガベージコレクションのあるC#などは非常にうまく言語に匿名関数がなじんでいます。ネイティブ言語の宿命といえばそうなのかもしれませんね。
しかしながら
・コールバックとして
・visitorパターンの代用として
・strategyパターンの代用として
・関数型プログラミングとして
等非常に強力な場面も多いです。注意深く使っていきたいですね。