Objective-CのBlocksの循環参照に関する僕なりのベストプラクティス

Objective-CのBlocksの循環参照とその解決策」では、Blocksのメモリ管理の難しさと、世間ではよく使われている「weakSelfパターン」があまり良くないのではないかということを書きました。
このブログでは、いろいろ考えた結果たどりついた、僕が考えるBlocksの使い方を書きたいと思います。
サンプルをgithubに載せましたので、良ければそちらも参照してください。( BlocksMemoryExample )

Blocksを安全で便利に使うために必要なこと

先に、Blocksを安全で便利に使うためには、どんなことが必要なのかというのを考えておきたいと思います。

  • 循環参照におちいらず、使用後に解放される。
  • 使い手が内部の実装をできるだけ意識する必要がない。
  • おまじない的なコードを書く必要がない。
  • できるだけBlocksに渡すオブジェクトの存在を保証する。

そして、Blocksの循環参照から逃れるために良く使われる「weakSelfパターン」は、このうちの3つ目と4つ目を満たせないので、あまり良くないのではないかということを「Objective-CのBlocksの循環参照とその解決策」に書きました。

使い手側に意識させない

僕が考えるBlocksのベストプラクティスは、Blocksの使い手側に「循環参照しないだろうか?」とか「本当にちゃんと解放されるんだろうか?」と、意識させないようにすることに尽きます。
そのためには「Blocksを渡された側、実際にそれを実行する側が、ちゃんと解放処理をおこなう」というのが原則になります。

// Bad Sample
BlocksRunner *blocksRunner = [[[BlocksRunner alloc] init] autorelease];
blocksRunner.runnable = ^{
    NSLog(@"blocksRunner: %@", blocksRunner);
};
[blocksRunner run];


これは循環参照してメモリリークする例です。ですが、Blocksの使い方は基本的にこのままでも問題ない状態になってくれるのが、ベストですよね。そのために、BlocksRunnerの実装側でメモリリークしないように対策していきます。

その場限りのBlocks: Blocksを保持しない場合

Blocksの使い方は大きく分けて3種類に分かれますので、順番に実装方法を書いていきます。
まずは、Blocksを保持しない場合です。いわゆるautoreleaseのような状態で、その場限りのBlocksです。InstantBlocksRunnerと呼ぶことにします。

// InstantBlocksRunner
InstantBlocksRunner *instantBlocksRunner  = [[[InstantBlocksRunner alloc] init] autorelease];
[instantBlocksRunner run: ^{
    NSLog(@"instantBlocksRunner: %@", instantBlocksRunner);
}];

このようにInstantBlocksRunnerインスタンスに、InstantBlocksRunnerインスタンスを渡しているので、一瞬循環参照を疑うかもしれませんが、これは全く問題のないパターンです。

- (void)run:(void (^)(void))runnable {
    if(runnable)
        runnable();
}

これが実装側です。このように、Blocksを引数などに渡したとしても、受け取った側が保持しなければ、当然循環参照にはなりません。
その場限りのBlocks: 「Blocksを保持しない場合は、特に意識する必要はない。」

1度きりのBlocks: 1度だけBlocksを使う場合

続いて、1度だけBlocksを用いる場合です。OneTimeBlocksRunnerと呼ぶことにします。
これは、たとえばインターネットからデータをダウンロードし、その結果をコールバックするなど、時間のかかる処理を非同期的に実行する場合などに用いられます。インターネットからデータをダウンロードする処理は、1つのファイルにつき1回きりなので、このパターンになります。

// OneTimeBlocksRunner
OneTimeBlocksRunner *oneTimeBlocksRunner = [[[OneTimeBlocksRunner alloc] init] autorelease];
oneTimeBlocksRunner.runnable = ^{
    NSLog(@"oneTimeBlocksRunner: %@", oneTimeBlocksRunner);
};
[oneTimeBlocksRunner run];


この場合ももちろん、使い手は循環参照を意識しません。
ただ、この場合は、インターネットからデータがダウンロードできるまでの間、Blocksを保持しておく必要があります。そこで、OneTimeBlocksRunner側で適切に解放処理をする必要があります。

- (void)run {
    if(runnable)
        runnable();
    self.runnable = nil;
}

具体的には、Blocksの実行がなされた直後にBlocksを解放することです。この実行がなされるまでは循環参照の状態になりますが、生涯で1度だけBlocksが実行されるタイミングが訪れるので、そのタイミングでBlocksを解放し、循環参照から逃れることができます。
1度きりのBlocks: 「1度だけBlocksを使う場合は、使った直後に解放する。」

使いまわすBlocks: 何度もBlocksを使う場合

前回のブログでも書いたように、曲者なのは1つのBlocksを何度も使う場合です。MultiTimeBlocksRunnerと呼ぶことにします。
何度も使う必要があるので、いつ解放してよいか分からないのが問題です。
結論から言うと、この使用法で循環参照にしてしまうと、意識せずに解放させることはできません。そこで、ここだけは弱参照に逃げます。

@property (nonatomic, copy) void(^runnable)(id target);
@property (nonatomic, assign) id target;
- (void)setRunnable:(void(^)(id target))newRunnable target:(id)newTarget {
    self.runnable = newRunnable;
    self.target = newTarget;
}

ただし、オブジェクトを弱参照で保持することを決めるのはMultiTimeBlocksRunnerの側です。使い手側は、MultiTimeBlocksRunnerのインターフェースの通りに使うだけで良いようにします。

// MultiTimeBlocksRunner
MultiTimeBlocksRunner *multiTimeBlocksRunner = [[[MultiTimeBlocksRunner alloc] init] autorelease];
[multiTimeBlocksRunner setRunnable:^(id target){
    NSLog(@"multiTimeBlocksRunner: %@", target);
} target:multiTimeBlocksRunner];
[multiTimeBlocksRunner run];
[multiTimeBlocksRunner run];
[multiTimeBlocksRunner run];


Blocksを渡す際に、同時に弱参照で保持したいインスタンスを渡します。Blocksの引数で弱参照のインスタンスを使えるようにするので、Blocks内部は今まで通り書くことができます。
使いまわすBlocks: 「何度もBlocksを使う場合は、オブジェクトを弱参照で保持できるオプションを用意する。」

サンプルの実行結果

悪い例のBlocksRunnerと、今回紹介した3つのパターンを実行しました。

BlocksRunner init
blocksRunner retainCount: 1
blocksRunner retainCount: 2
blocksRunner: 
InstantBlocksRunner init
instantBlocksRunner retainCount: 1
instantBlocksRunner: 
OneTimeBlocksRunner init
oneTimeBlocksRunner retainCount: 1
oneTimeBlocksRunner retainCount: 2
oneTimeBlocksRunner: 
MultiTimeBlocksRunner init
multiTimeBlocksRunner retainCount: 1
multiTimeBlocksRunner retainCount: 1
multiTimeBlocksRunner: 
multiTimeBlocksRunner: 
multiTimeBlocksRunner: 
MultiTimeBlocksRunner dealloc
OneTimeBlocksRunner dealloc
InstantBlocksRunner dealloc

悪い例のBlocksRunner以外の3つのインスタンスは、最後に適切に解放されています。

結局、弱参照に逃げてるじゃん?

最後は結局、弱参照に逃げてしまいました。ただ、これは「weakSelfパターン」とは全く違うことを補足しておきたいと思います。
「weakSelfパターン」は全てのBlocksに対して、インスタンスを弱参照で渡す必要があります。ここでBlocksの使い方には3種類あると書きましたが、Blocksが循環参照するのは3番目の場合だけにも関わらず、実装がどうなっているか分からない以上、全ての場合に弱参照を用いる必要があります。
今回紹介したようなBlocksの使い方をすることで、本当に必要な時だけ弱参照の逃げ道を使い、そうでない場合は安全で便利な記述をすることができます。外部のライブラリなどは実装を意識して使う必要がありますが、気に入らなければ今回紹介したような手法でラッパーを作るのもありだと思います。

idだと型が不明なので使いにくい!

弱参照で保持するインスタンスをid型で引数に渡すようにしていますが、これだと型が不明なのでコンパイル警告が出たり使いにくい場合があります。
そこで、使い手側で以下のようにtypeofで型を特定してやると良いかもしれません。

self.multiTimeBlocksRunner = [[[MultiTimeBlocksRunner alloc] init] autorelease];
[multiTimeBlocksRunner setRunnable:^(typeof(self) self){
    NSLog(@"multiTimeBlocksRunner: %@", self);
} target:self];

長くなりましたが、Blocksを便利に使って楽しくプログラミングをしましょう!

コメント

  1. […] Post navigation ← Previous Next → […]

  2. […] Objective-CのBlocksの循環参照とその解決策 Objective-CのBlocksの循環参照に関する僕なりのベストプラクティス […]

タイトルとURLをコピーしました