203
199

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

blocksの落とし穴

Last updated at Posted at 2012-12-26

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

203
199
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
203
199

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?