Objective-CのBlocksの循環参照とその解決策

半年までは、Objective-Cでコールバックの処理は、delegateを使って実装していました。しかし、delegateによるコールバックは処理の繋がりが分かりにくいというデメリットがあります。
これは皆さん感じるところだと思いますが、実際に最近のObjective-C開発は、Blocksを使って記述するのがトレンドだと思います。
ただ、メモリ管理に関して少し注意しなければならない点があるので、検証結果も合わせて記していきたいと思います。(少し長くなるので2回にわけて書きたいと思います。)

Blocksで処理を行う

これは簡単なサンプルです。BlocksRunnerクラスは、Blocksで渡された処理を保持し、runメソッドが呼ばれた時にその処理を実行します。

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        BlocksRunner *blocksRunner = blocksRunner = [[[BlocksRunner alloc] init] autorelease];
        NSLog(@"blockRunner retainCount: %ld", [blocksRunner retainCount]);
        blocksRunner.runnable = ^{
            NSLog(@"blocksRunner");
        };
        NSLog(@"blockRunner retainCount: %ld", [blocksRunner retainCount]);
        [blocksRunner run];
    }
    return 0;
}
BlocksRunner init
blockRunner retainCount: 1
blockRunner retainCount: 1
blocksRunner
BlocksRunner dealloc

blocksRunnerの参照カウントは、生成時にカウントされた「1」です。処理後にはautoreleaseが呼ばれて、参照カウントが減らされ、BlocksRunnerも解放されます。

Blocksの循環参照問題

Blocksの扱いが難しいのは、Blocksの内部で使う変数をBlocks自身が保持するため、循環参照におちいる場合があることです。

blocksRunner.runnable = ^{
    NSLog(@"blocksRunner: %@", blocksRunner);
};


前のソースコードとの違いは、Blocks内部でその呼び出し元であるBlocksRunnerインスタンスを呼び出していることです。

BlocksRunner init
blockRunner retainCount: 1
blockRunner retainCount: 2
blocksRunner: 

この場合、最後の1行がなくなっている点がポイントです。つまり、処理後にBlocksRunnerインスタンスが解放されていません。
参照カウントが「2」になっているのが注目する点です。BlocksにBlocksRunner自身を渡す際に参照カウントが増えてしまいます。これは、BlocksRunnerインスタンスが処理するBlocksを保持していて、そのBlocksが内部のログ出力のためにBlocksRunnerインスタンスを保持している状態です。
このようにBlocks内部で、Blocksの呼び出し元を呼び出してしまうと、循環参照におちいり、Blocksが解放されません。

弱参照のインスタンスを渡す

この手の問題の解決策として良く用いられているのは、弱参照のインスタンスを渡すという手法です。
Blocksで循環参照におちいるのはBlocks内部でselfを呼ぶ場合が多いので、「weakSelfパターン」と呼んでも良いんじゃないかと思うくらい浸透しています。

__block BlocksRunner *weakBlocksRunner = blocksRunner;
blocksRunner.runnable = ^{
    NSLog(@"blocksRunner: %@", weakBlocksRunner);
};


ここでは、blocksRunner変数を別のweakBlocksRunner変数にコピーしています。__block修飾子をつけることで、このweakBlocksRunner変数は、Blocksによって参照カウントが増加されません。

BlocksRunner init
blockRunner retainCount: 1
blockRunner retainCount: 1
blocksRunner: 
BlocksRunner dealloc

これによって、循環参照から解放され、無事にBlocksRunner自身を解放できます。

弱参照を使うことの問題点

この手法は本当に多く人が用いていて、かなりスタンダードな手法なのですが、僕はあまり良い方法ではないと思っています。
この手法の問題点として考えるのは、まずソースコードに変数のコピー処理を書かなければならないことがあります。しかも、__blockとか__weakとかいういかにも怪しげな修飾子つきで書かなければならないことです。
第2に、循環参照は必ずしも起きるものではないのに、起きるか起きないかが分からない以上、この変数のコピー処理を必ず書かなければなりません。実際にBlocksを使ってみると、循環参照が起きる状況はそれほど多くありません。にも関わらず、このweakSelfパターンは全てのBlocksに適用しないと、全てが循環参照しないとは保証できません。
最後に、「そもそもなぜ、Blocksは内部で使う変数を参照して保持してしまうのか?」ということを考えると、当然必要だから保持しています。具体的には、Blocksは「内部で使用する変数が確実に存在しつづけていることを保証する」ために参照カウントを増やしています。この弱参照を用いる手法は、この目的に逆らうもので、Blocks内部で用いる変数が「解放されていないことを保証することができない」という問題があります。

BlocksRunnerが使用後にBlocksを解放する

上記のような問題点を回避するためには、意識をしなくても循環参照におちいらず、弱参照も利用しなくて済むような設計をする必要があります。
ひとつの手段としては、BlocksRunner、ようするにBlocksを実際に使用する側で、使用後にBlocksを解放してやることができます。

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

runメソッドが呼ばれて、Blocksが呼ばれたら、適切に解放処理をおこなっています。Blocksが解放されれば、Blocksが保持していたBlocksRunner自身も適切に解放されます。

BlocksRunner init
blockRunner retainCount: 1
blockRunner retainCount: 2
blocksRunner: 
BlocksRunner dealloc

この通り、この手法では、参照カウントは一時的に「2」になるものの、最終的には適切に解放されます。
ただこの手法には一点問題があります。Blocksを1度呼び出したあとに解放してしまうので、2度と呼び出すことができないということです。ただBlocksが使いたくなる場合の半分くらいは、1度呼び出したら終わりなのでこの手法で対応できます。

BlocksRunnerの呼び出し元が使用後にBlocksを解放する

Blocksを呼び出し元が保持していて、循環参照におちいっていて、なおかつ複数回呼び出す必要がある場合、これが問題です。

BlocksRunner *blocksRunner = blocksRunner = [[[BlocksRunner alloc] init] autorelease];
NSLog(@"blockRunner retainCount: %ld", [blocksRunner retainCount]);
blocksRunner.runnable = ^{
    NSLog(@"blocksRunner: %@", blocksRunner);
};
NSLog(@"blockRunner retainCount: %ld", [blocksRunner retainCount]);
[blocksRunner run];
[blocksRunner run];
[blocksRunner run];
blocksRunner.runnable = nil;


この場合は、BlocksRunner側で勝手にBlocksを解放してしまうことはできないので、呼び出し側で解放する必要があります。

BlocksRunner init
blockRunner retainCount: 1
blockRunner retainCount: 2
blocksRunner: 
blocksRunner: 
blocksRunner: 
BlocksRunner dealloc

しかし、これは呼び出し元が、BlocksRunnerの内部の実装を意識していなければならず、あまり良い方法とはいえません。あとは呼び出し元すらもいつ解放して良いか分からないという場合もあり、その場合はこの手法を使うことはできません。
色々考えた結果、僕なりの一つのパターンに至ったので、それについては「Objective-CのBlocksの循環参照に関する僕なりのベストプラクティス」をぜひ見てください。

コメント

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

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

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