Powered By Blogger

2012年4月21日土曜日

Blocks関連のバグがデバッグコンパイルで再現しないケース

TOMOHISAさんのブログ記事
BLOCKSを使った記述で、リリースビルドのみにクラッシュする事例
これは元のコードの問題はスタック領域上のブロックオブジェクトをスコープ外で使ってしまったという割と有りがちなバグだったのですが、 念のため検証してみました。

ところがブロックオブジェクトのデフォルトの生成場所(スタック領域かヒープ領域か)がデバッグコンパイル時にはどうも違うらしくて、 そういう挙動を知らずにデバッグコンパイルで検証してしまった為にハマってしまいました。

今回のはちゃんとリリースコンパイルでもテストしないといけないという教訓です。

ブロックのデフォルトの生成場所はスタック領域なのですが、デバッグだろうがリリースだろうがそれは同じだと思い込んでいました。

特に__block属性付き変数をキャプチャするとブロックの生成場所は最初からヒープになるみたいです。何でかな?…

検証コード
コードはTOMOHISAさんのものですが、
NSLog(〜〜,block) の箇所とコメントは私が検証の為に追加しました。
因みに_tweetはこのコードが属するオブジェクトのメンバ関数です。
ARCです。
//
    NSMutableArray *arrRows = [NSMutableArray arrayWithCapacity:0];
    { //検証コード1
#warning this code only crash on Release Build.... Don't use this
        NSMutableDictionary * dicRow = [NSMutableDictionary dictionaryWithCapacity:0];
        [dicRow setValue:NSLocalizedString(@"Pattern 1 Crash",nil) forKey:kDicKeyLinkPopCellText];
        [arrRows addObject:dicRow];
        dispatch_block_t block = ^{
            
            NSString *str = [NSString stringWithFormat:@"%@",[_tweet valueForKey:@"text"]];
            
            [[UIPasteboard generalPasteboard] setString:str];
            [[JTCAppNotificationManager sharedManger] startTimerNotificationWithMessage:NSLocalizedString(@"Copy succeeded", @"Copy succeeded") dulation:2.5 iconName:@"w17-check.png"];

        };
        NSLog(@"Pattern 1 %@",block);
        [dicRow setValue:block forKey:kDicKeyLinkPopBlock];
    }
    { //検証コード2
        NSMutableDictionary * dicRow = [NSMutableDictionary dictionaryWithCapacity:0];
        [dicRow setValue:NSLocalizedString(@"Pattern 2 __block",nil) forKey:kDicKeyLinkPopCellText];
        [arrRows addObject:dicRow];
#warning this code is not the way recommended how to use __block ... not recommended
        __block id bt = _tweet;
        dispatch_block_t block = ^{
            
            NSString *str = [NSString stringWithFormat:@"%@",[bt valueForKey:@"text"]];
            
            [[UIPasteboard generalPasteboard] setString:str];

            [[JTCAppNotificationManager sharedManger] startTimerNotificationWithMessage:NSLocalizedString(@"Copy succeeded", @"Copy succeeded") dulation:2.5 iconName:@"w17-check.png"];
            
        };
        NSLog(@"Pattern 2 %@",block);
        [dicRow setValue:block forKey:kDicKeyLinkPopBlock];
    }
    { //検証コード3
        NSMutableDictionary * dicRow = [NSMutableDictionary dictionaryWithCapacity:0];
        [dicRow setValue:NSLocalizedString(@"Pattern 3 only declare bt crash",nil) forKey:kDicKeyLinkPopCellText];
        [arrRows addObject:dicRow];
#warning this code only crash on Release Build.... Don't use this
        id bt = _tweet;
        dispatch_block_t block = ^{
            
            NSString *str = [NSString stringWithFormat:@"%@",[bt valueForKey:@"text"]];
            
            [[UIPasteboard generalPasteboard] setString:str];
            
            [[JTCAppNotificationManager sharedManger] startTimerNotificationWithMessage:NSLocalizedString(@"Copy succeeded", @"Copy succeeded") dulation:2.5 iconName:@"w17-check.png"];
        };
        NSLog(@"Pattern 3 %@",block);
        [dicRow setValue:block forKey:kDicKeyLinkPopBlock];
    }
    { //検証コード4
        NSMutableDictionary * dicRow = [NSMutableDictionary dictionaryWithCapacity:0];
        [dicRow setValue:NSLocalizedString(@"USE THIS:Pattern 4 declare bt and copy block",nil) forKey:kDicKeyLinkPopCellText];
        [arrRows addObject:dicRow];
        id bt = _tweet;
        dispatch_block_t block = ^{
            
            NSString *str = [NSString stringWithFormat:@"%@",[bt valueForKey:@"text"]];
            
            [[UIPasteboard generalPasteboard] setString:str];
            
            [[JTCAppNotificationManager sharedManger] startTimerNotificationWithMessage:NSLocalizedString(@"Copy succeeded", @"Copy succeeded") dulation:2.5 iconName:@"w17-check.png"];
        };
        NSLog(@"Pattern 4 %@",block);
        [dicRow setValue:[block copy] forKey:kDicKeyLinkPopBlock];
    }
    
    NSMutableArray *sections = [NSMutableArray arrayWithObject:arrRows];
    TOVLinkPopoverViewController *controller= [[TOVLinkPopoverViewController alloc] init];
    controller.arrayLink = sections;




デバッグビルドでのログ出力

2012-04-21 13:49:22.109 TOVPopoverSample[10592:707] Pattern 1 <__NSMallocBlock__: 0xfe44ee0>
2012-04-21 13:49:34.734 TOVPopoverSample[10592:707] Pattern 2 <__NSMallocBlock__: 0xfe45b20>
2012-04-21 13:49:39.624 TOVPopoverSample[10592:707] Pattern 3 <__NSMallocBlock__: 0x13dc50>
2012-04-21 13:49:43.224 TOVPopoverSample[10592:707] Pattern 4 <__NSMallocBlock__: 0xfe45110>

見ての通り4パターンともmallocされたブロック、つまりヒープ上に生成されてます。 これではバグの検証は出来ませんね。
別に全部問題ないじゃん、みたいに見えてしまいます。
「ほらスタック領域にブロック生成されるからヒープにコピーしないと」と言う結論を期待したのに「なんで…」と数時間悩んでしまいました。


リリースビルドでのログ出力

2012-04-21 13:10:39.912 TOVPopoverSample[10504:707] Pattern 1 <__NSStackBlock__: 0x2fe54510>
2012-04-21 13:10:54.860 TOVPopoverSample[10504:707] Pattern 2 <__NSMallocBlock__: 0x149250>
2012-04-21 13:11:18.754 TOVPopoverSample[10504:707] Pattern 3 <__NSStackBlock__: 0x2fe544c4>
2012-04-21 13:12:11.226 TOVPopoverSample[10504:707] Pattern 4 <__NSStackBlock__: 0x2fe544ac>

ほぼ予想通りStackBlockとなってます。
このスタックブロックを辞書に登録しておいてスコープ外で使おうとしているのでバグだね、と分かる訳です。
ただパターン2、__block属性の変数をキャプチャしたケースだけはMallocBlockとなってます。
なぜこのケースではヒープに生成されるのか理由は分かりませんが、この為に問題無いかの様に動いたのですね。


結論
Blocksはデバッグビルドでのテストだけで満足してはならない。
リリースビルドでも入念にテストすべきである。


因みに。。。
このケースでは正解は4なのですが、いつもコピーする方が良い訳ではないので注意が必要です。
ヒープにコピーしたらしたで、リークだとか別の心配をしないといけなくなりますから。
4ではブロックをスコープ外で使いたい為にヒープにコピーしています。
ただし!!
ヒープにコピーしたブロックを使い終わったら忘れない様にリリースしないといけません。
今回のはARCなので問題なさそうではありますが。

__blockをキャプチャするとヒープブロックになるのを知ってたのなら2も正解じゃん、
という意見もあるかも知れませんが、そういう裏ワザ的なのはここでは不正解とさせて頂きます(笑)