3

块存储管理的一个边缘壳体下面的代码会崩溃,因为EXC_BAD_ACCESS理解objc

typedef void(^myBlock)(void); 

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    NSArray *tmp = [self getBlockArray]; 
    myBlock block = tmp[0]; 
    block(); 
} 

- (id)getBlockArray { 
    int val = 10; 
//crash version 
    return [[NSArray alloc] initWithObjects: 
      ^{NSLog(@"blk0:%d", val);}, 
      ^{NSLog(@"blk1:%d", val);}, nil]; 
//won't crash version 
// return @[^{NSLog(@"block0: %d", val);}, ^{NSLog(@"block1: %d", val);}]; 
} 

代码在IOS 9 ARC运行启用。我试图找出导致崩溃的原因。

通过po tmp在LLDB我发现

(lldb) po tmp 
<__NSArrayI 0x7fa0f1546330>(
<__NSMallocBlock__: 0x7fa0f15a0fd0>, 
<__NSStackBlock__: 0x7fff524e2b60> 
) 

而在不会崩溃版本

(lldb) po tmp 
<__NSArrayI 0x7f9db481e6a0>(
<__NSMallocBlock__: 0x7f9db27e09a0>, 
<__NSMallocBlock__: 0x7f9db2718f50> 
) 

所以最可能的原因,我能想出是当ARC释放NSStackBlock崩溃发生。但为什么会这样呢?

回答

1

首先,你需要明白,如果你想存储块过去​​在那里的申报范围,则需要将其复制并保存复印件。

由于优化的原因,其中捕获变量的块最初位于堆栈上,而不是像常规对象那样动态分配。 (让我们忽略那些暂时不捕获变量的块,因为它们可以作为一个全局实例来实现)。因此,当你编写一个块文字时,如foo = ^{ ...};,这就好像为foo赋值一个指向隐藏局部变量的指针在同样的范围内,像some_block_object_t hiddenVariable; foo = &hiddenVariable;这样的优化可以减少很多情况下对象分配的数量,在这种情况下,块被同步使用,永远不会超出创建它的范围。

与指向局部变量的指针一样,如果您将指针放在指向它的事物范围之外,则您有一个悬挂指针,并且解除引用会导致未定义的行为。如果需要,在块上执行复制将堆栈移到堆上,像所有其他Objective-C对象一样进行内存管理,并返回指向堆副本的指针(并且如果块已经是堆块或全局块它只是返回相同的指针)。

特定编译器在特定情况下是否使用这种优化是一种实现细节,但您不能假定它是如何实现的,因此如果您将块指针存储在一个将超过当前范围(例如,在实例或全局变量中,或者在可能超过范围的数据结构中)。即使你知道它是如何实现的,并且知道在特定情况下复制不是必需的(例如,它是一个不捕获变量的块,或者必须已经完成复制),你不应该依赖它,并且作为良好的做法,当你将它存储在一个会超过当前范围的地方时,你仍应该始终进行复制。

将块作为参数传递给函数或方法有点复杂。如果将块指针作为参数传递给其声明的编译时类型为块指针类型的函数参数,那么该函数将负责复制该函数,以使其超出范围。所以在这种情况下,你不需要担心复制它,而不需要知道函数做了什么。

另一方面,如果您将块指针作为参数传递给声明的编译时类型为非块对象指针类型的函数参数,那么该函数将不承担任何块的责任复制,因为它知道它只是一个普通的对象,如果存储在超过当前范围的地方,只需要保留。在这种情况下,如果您认为该函数可能将值存储在调用结束之后,则应该在传递该块之前先复制该块,然后传递该副本。顺便说一下,对于块指针类型被分配或转换为常规的对象指针类型的情况,这也是正确的。应该复制该块并分配副本,因为任何获得常规对象指针值的人都不希望执行任何块复制注意事项。


ARC在某种程度上使情况复杂化。 ARC规范specifies某些情况下块被隐式复制。例如,当存储到编译时块指针类型的变量(或ARC需要在编译时块指针类型的值上保留的任何其他位置)时,ARC要求复制传入值而不是保留,所以程序员不必担心在这些情况下显式复制块。

随着保留做作为初始化 __strong参数变量或阅读__weak变量的一部分外,每当 这些语义调用用于保持块指针类型的值,它 具有Block_copy的效果。

但是,作为一个例外,ARC规范并不保证仅当参数被复制时才传递块。

当它看到结果是 仅用作调用的参数时,优化程序可能会删除这些副本。

因此,无论明确复制作为参数传递给一个功能块却仍然是程序员必须考虑的问题。

现在,在最近版本的Apple Clang编译器中的ARC实现有一个未公开的功能,它将向块的一些地方添加隐式块拷贝作为参数,即使ARC规范不需要它。 (“无证”,因为我找不到任何Clang文档来达到这种效果)。特别是,在将块指针类型的表达式传递给非块对象指针类型的参数时,它似乎总是在防御中添加隐式副本。实际上,如CRD所示,它在从块指针类型转换为常规对象指针类型时也添加了隐式副本,所以这是更一般的行为(因为它包含参数传递大小写)。

但是,看起来Clang编译器的当前版本在将块指针类型的值作为可变参数传递时不会添加隐式副本。 C可变参数不是类型安全的,调用者不可能知道函数期望的类型。可以说,如果苹果想要在安全方面犯错误,由于无法知道函数期望的是什么,所以在这种情况下,它们总是应该添加隐式副本。然而,由于这整个事情无论如何都是无证的功能,我不会说这是一个错误。在我看来,程序员不应该依赖只能作为参数被隐式复制的块传递。

+0

感谢您的详细解答。我相信C可变参数不是类型安全的,这是对这种情况更准确的解释。 – dopcn

+0

@dopcn - newacct我倾向于不同意规范在块和ARC方面的状态。不幸的是,苹果的文档并不总是那么清晰和全面,并且不仅仅是公平的,而且涉及到一定程度的解释。请将您的案例作为一个错误提交给Apple;他们可以修复它,说它按预期工作,或者什么都不说;但你会提醒他们。如果他们确实回复了有用的回复,可以将其添加到上面的问题中作为帮助其他人的附录。 – CRD

3

简答

你已经找到一个编译器缺陷,可能重新引进一个,你应该在http://bugreport.apple.com报告。

再回应

这并非总是一个错误,它曾经是一个功能 ;-)当苹果首次推出块他们也介绍了他们是如何实现他们的优化;然而,与对代码基本透明的普通编译器优化不同,它们要求程序员在各个地方将调用分配给特殊函数block_copy()以使优化工作。

多年来,Apple取消了对此的需求,但仅限于使用ARC的程序员(尽管他们本可以为MRC用户也这样做),而今天的优化应该只是这样,程序员不再需要帮助编译器一起。

但是你刚刚发现了一个编译器错误的例子。

技术上必须的情况下的类型损失,在这种情况下一些已知为块作为id传递 - 减少了已知类型的信息,并在特定类型的损失,涉及在一个变量的第二个或后续的参数参数列表。当你用po tmp来查看你的数组时,你会发现第一个值是正确的,尽管存在类型丢失,编译器仍然可以正确地判断这个值,但是它在下一个参数上失败。

数组的字面语法不依赖于可变参数函数,并且生成的代码是正确的。但initWithObjects:确实会出错,并且会出错。

解决方法

如果强制转换为id添加到第二(和任何随后的)块则编译器产生正确的代码:

return [[NSArray alloc] initWithObjects: 
     ^{NSLog(@"blk0:%d", val);}, 
     (id)^{NSLog(@"blk1:%d", val);}, 
     nil]; 

这似乎是足以唤醒编译起来。

HTH

+0

解决方法已通过验证。感谢您的回答。但我想知道更多关于碰撞原因的信息。如果没有类型转换,'NSStackBlock'也是有效的,并在调用时运行。为什么释放它会导致崩溃?还是别的会导致崩溃? – dopcn

+0

'NSStackBlock'不是一个普通的对象 - 它是上面提到的优化的结果 - 它永远不应该存储在一个数组(或任何其他对象)中。它仅作为参数传递给方法,并且只在调用方(即创建它传递给另一方的方法)在调用堆栈上仍然有效时才起作用。违反任何这些规则,所有的赌注都没有了,编译器不会帮助你。 – CRD