gpt4 book ai didi

ios - 了解objc中 block 存储管理的一种极端情况

转载 作者:可可西里 更新时间:2023-11-01 17:14:38 26 4
gpt4 key购买 nike

以下代码由于EXC_BAD_ACCESS而崩溃

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);}];
}

该代码在启用ARC的iOS 9中运行。而且我正试图找出导致崩溃的原因。

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

而在不会崩溃的版本中
(lldb) po tmp
<__NSArrayI 0x7f9db481e6a0>(
<__NSMallocBlock__: 0x7f9db27e09a0>,
<__NSMallocBlock__: 0x7f9db2718f50>
)

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

最佳答案

首先,您需要了解,如果要存储超出声明范围的块,则需要复制该块并存储该副本。

这样做的原因是由于优化,捕获变量的块最初位于堆栈上,而不是像常规对象那样动态分配。 (让我们忽略当前暂时不捕获变量的块,因为它们可以作为全局实例实现。)因此,当您编写块文字(如foo = ^{ ...};)时,实际上就像为foo分配了指向已声明的隐藏局部变量的指针一样在相同的作用域中,类似于some_block_object_t hiddenVariable; foo = &hiddenVariable;这样的优化在许多情况下可以减少对象分配的数量,这些情况是同步使用块的,并且永远不会超过创建它的作用域。

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

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

将块作为参数传递给函数或方法有点复杂。如果将块指针作为参数传递给函数参数,该函数参数的声明编译时类型为块指针类型,则该函数将负责复制它,如果它的作用域超出其范围。因此,在这种情况下,您无需担心复制它,而无需知道函数的功能。

另一方面,如果将块指针作为参数传递给声明为编译时类型为非块对象指针类型的函数参数,则该函数将不承担任何块复制责任,因为它所知道的只是一个常规对象,如果将其存储在超出当前范围的地方,则只需保留该对象即可。在这种情况下,如果您认为函数可能在调用结束后存储了该值,则应在传递该块之前复制该块,然后传递该副本。

顺便说一句,在将块指针类型分配或转换为常规对象指针类型的任何其他情况下也是如此。应该复制该块并分配副本,因为不会期望获得常规对象指针值的任何人进行任何块复制注意事项。

ARC使情况变得有些复杂。 ARC规范specifies在某些情况下隐式复制了块。例如,当存储到编译时块指针类型的变量(或ARC需要保留编译时块指针类型的值的任何其他地方)时,ARC要求复制输入值而不是保留,因此程序员在这种情况下不必担心显式复制块。

除了保留是初始化a的一部分
每当有__strong参数变量或读取__weak变量
这些语义要求保留块指针类型的值,
具有Block_copy的效果。

但是,作为例外,ARC规范不保证复制仅作为参数传递的块。

当优化程序看到结果是
仅用作呼叫的参数。

因此,是否要显式复制作为参数传递给函数的块仍然是程序员必须考虑的事情。

现在,Apple Clang编译器的最新版本中的ARC实现具有一个未记录的功能,即使ARC规范不需要它,它也会在将块作为参数传递的某些位置添加隐式块副本。 (“undocumented”(因为未找到任何实现此目的的Clang文档。)尤其是,当将块指针类型的表达式传递给非块对象指针类型的参数时,它似乎总是在防御性地添加隐式副本。实际上,正如CRD所演示的那样,当从块指针类型转换为常规对象指针类型时,它还会添加一个隐式副本,因此这是更通用的行为(因为它包括参数传递的大小写)。

但是,当将块指针类型的值作为varargs传递时,当前版本的Clang编译器似乎未添加隐式副本。 C varargs不是类型安全的,调用者不可能知道函数期望的类型。可以说,如果Apple要在安全方面犯错误,由于无法知道功能的期望,因此在这种情况下,他们也应始终添加隐式副本。但是,由于整件事都是未记录的功能,因此我不会说这是一个错误。我认为,程序员永远不要依赖仅作为隐式复制的参数传递的块。

关于ios - 了解objc中 block 存储管理的一种极端情况,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35822645/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com