blocksの落とし穴

  • 208
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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パターンの代用として
・関数型プログラミングとして
等非常に強力な場面も多いです。注意深く使っていきたいですね。