ARC 下向 NSArray 添加 Block 元素的一个小坑

原文地址:http://matrixzk.github.io/blog/20150518/store_blocks_in_NSArray/

一直以来我都认为在 ARC 下,给 Cocoa 框架的集合类,如 NSArray,添加 Block 类型的元素时,Block 是会被编译器自动执行 copy 操作的。而且一直以来的实践也验证了这一事实。但今天在测试如下一段代码时出现了问题。

问题描述

先看下出问题的测试代码:

id getBlockArray() {
    int val = 12;
    NSArray *arr = [[NSArray alloc] initWithObjects:^{NSLog(@"block1 val = %d", val);},
                                                    ^{NSLog(@"block2 val = %d", val);},
                                                    nil];
    return arr;
}

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    id blkArray = [obj getBlockArray];
    MyBlock block1 = blkArray[0];
    MyBlock block2 = blkArray[1]; // EXC_BAD_ACCESS, crash !!!

    blcok1();
    blcok2();
    return 0;
}

如上所示,在获取数组中第二个 Block 元素时,crash 了,原因是 EXC_BAD_ACCESS,即访问了已被释放的无效内存。很奇怪,调试打印 arr,输出如下:

<__NSArrayM 0x100300500>(
    <__NSMallocBlock__: 0x100300410>,
    <__NSStackBlock__: 0x7fff5fbff750>
)

居然一个在堆上,一个在栈上,这。。。有些挑战三观了。

测试验证

确实很奇怪,那不妨来试试给数组填充元素的其他方式吧:

- (id)getBlockArray {
    int val = 10;

    NSMutableArray *arr = [[NSMutableArray alloc] init];
    [arr addObject:^{NSLog(@"block1 val = %d", val);}];
    [arr addObject:^{NSLog(@"block2 val = %d", val);}];

    return arr;
}

以及:

int val = 10;
NSArray *arr = @[^{NSLog(@"block1 val = %d", val);}, ^{NSLog(@"block2 val = %d", val);}];

这两种情况下调试打印 arr,输出如下:

<__NSArrayM 0x100100250>(
    <__NSMallocBlock__: 0x100200550>,
    <__NSMallocBlock__: 0x1003007e0>
)

可以看到都没问题,作为 addObject: 参数添加进来的两个 Block 元素,都被编译器自动执行了 copy 操作,这样 Block 的类型就变成了 __NSMallocBlock,被拷贝到了堆上。好险,三观稍微又正了点儿。但文章开头的问题究竟是什么原因呢?

探寻原因

比较上边的测试代码和出问题的代码,同样都是 ARC 的测试环境,为什么问题代码中数组的两个 Block 元素,第一个在堆上,第二个在栈上呢?联想到像测试代码中这样,将 Block 拷贝到堆上的操作是编译器在编译时完成的,那问题会不会出在初始化方法上呢?然后点开出问题的那个 API:

- (instancetype)initWithObjects:(id)firstObj, ... NS_REQUIRES_NIL_TERMINATION;

果然!这里的参数是可变个数的,而且只有第一个参数显式的声明为 id 类型。这下就能解释问题代码中,为什么第一个 Block 元素在堆上而第二个却在栈上:因为只有第一个参数显式的声明为 id 类型,所以编译器在编译阶段只能意识到需要对第一个作为参数传进来的 Block 进行 copy 处理。为了验证这一猜测,下面显式得把后边的 Block 传参强制转换为 id 类型,让编译器看到它:

NSArray *arr = [[NSArray alloc] initWithObjects:^{NSLog(@"block1 val = %d", val);},
                                                (id)^{NSLog(@"block2 val = %d", val);},
                                                nil];

代码顺利运行通过,没有 crash,猜测得到了验证。这真算是一个坑点儿。在 stackoverflow 上看到了一个对类似问题的讨论,可以参考下:Storing Blocks in an Array

扩展

另外需要注意的一点是,在 MRC 下,向方法或函数的参数中传递 Block 时,除了以下两种情况,都需要手动 copy 一下:

  • Cocoa 框架中的方法名含有 usingBlock 的方法。例如 NSArray 的 enumerateObjectsUsingBlock 实例方法。
  • GCD 的 API。例如,dispatch_async 函数。

在 ARC 下,除了上述两种情况外,在如下两种情况,编译器也帮我们自动做了 copy 操作:

  • Block 作为函数或方法的返回值返回时。(此场景和 ARC 下普通的对象作为函数或方法返回值返回时的场景一致)
  • 将 Block 赋值给附有 __strong 修饰符的变量时。(ARC 下的局部变量和成员变量默认都是 __strong的,只是作用域不同)

这里有一个有趣的小测试Objective-C Blocks Quiz,可以测下自己对 Block 的理解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容