我在使用Objective-C中的ARC时需要复制块的时候收到了相互矛盾的信息。建议不等于"始终"从来没有"所以我真的不知道该怎么做。
我碰巧有一个案例,我不知道如何解释:
-(RemoverBlock)whenSettledDo:(SettledHandlerBlock)settledHandler {
// without this local assignment of the argument, one of the tests fails. Why?
SettledHandler handlerFixed = settledHandler;
[removableSettledHandlers addObject:handlerFixed];
return ^{
[removableSettledHandlers removeObject:handlerFixed];
};
}
使用这样的块内联调用:
-(void) whatever {
[self whenSettledDo:^(...){
...
}];
}
(The actual code this snipper was adapted from is here.)
将参数复制到局部变量的内容会发生什么变化?没有本地的版本是否有两个不同的副本,一个用于addObject,另一个用于removeObject,因此删除的副本与添加的副本不匹配?
为什么或何时没有正确处理ARC?它保证了什么,我的责任是什么?所有这些都以非模糊的方式记录在哪里?
答案 0 :(得分:3)
在C中,无法通过运行任意数量的测试来推断正确性,因为您可能会看到未定义的行为。要正确了解什么是正确的,您需要参考语言规范。在这种情况下,ARC specification。
首次审核何时需要在MRC下复制块是有益的。基本上,捕获变量的块可以从堆栈开始。这意味着当您看到块文字时,编译器可以用包含对象结构本身的范围中的隐藏局部变量替换它。由于局部变量仅在声明它们的范围内有效,因此块文字中的块仅在文字所在的范围内有效,除非它被复制。
此外,还有一个附加规则,即如果函数采用块指针类型的参数,则不会假设它是否是堆栈块。只保证块在调用块时有效。但是,这几乎意味着该块在函数调用的整个持续时间内都是有效的,因为1)如果它是一个堆栈块,并且在调用该函数时它是有效的,这意味着该块在块的哪个位置。创建后,调用仍然在堆栈文字的范围内;因此,在函数调用结束时它仍然在范围内; 2)如果它是堆块或全局块,则它与其他对象具有相同的内存管理规则。
由此,我们可以推断出需要复制的地方。让我们考虑一些情况:
dispatch_async
)。-addObject:
):它需要是如果你知道这个函数存储它以供以后复制。它需要被复制的原因是该函数不能对复制块负责,因为它不知道它是在阻塞。因此,如果问题中的代码位于MRC中,则-whatever
不需要复制任何内容。 -whenSettledDo:
需要复制该块,因为它被传递给addObject:
,这是一种采用通用对象的方法,类型为id
,并且不知道它采取阻止。
现在,让我们来看看ARC为您提供的这些副本中的哪一个。 Section 7.5说
除了在初始化__strong时完成的保留 参数变量或读取__weak变量,只要这些 语义调用保留块指针类型的值,它具有 Block_copy的效果。优化器可以在删除时删除这些副本 看到结果仅用作呼叫的参数。
第一部分的含义是,在大多数地方,您指定了块指针类型的强引用(通常会导致保留对象指针类型),它将被复制。但是,有一些例外:1)在第一句的开头,它表示不保证复制块指针类型的参数; 2)在第二句中,它表示如果一个块仅用作一个调用的参数,则不能保证它被复制。
这对您问题中的代码意味着什么? handlerFixed
是块指针类型的强引用,结果在两个地方使用,不仅仅是一个调用的参数,因此赋值给它一个副本。但是,如果您已将块文字直接传递给addObject:
,则无法保证副本(因为它仅用作调用的参数),您需要复制它明确地(正如我们所讨论的那样,传递给addObject:
的块需要被复制)。
直接使用settledHandler
时,由于settledHandler
是参数,因此不会自动复制,因此当您将其传递给addObject:
时,需要明确复制它,因为我们讨论了传递给addObject:
的块需要复制。
总而言之,在ARC中,您需要在将块传递给不具体使用块参数的函数时显式复制(如addObject:
),如果它&# 39; sa block literal,或者它是你传递的参数变量。
答案 1 :(得分:0)
我已经确认我的特定问题实际上是制作了两个不同的块副本。棘手的棘手。这意味着正确的建议是“永远不要复制,除非您希望能够将块与自身进行比较”。
以下是我用来测试它的代码:
-(void) testMultipleCopyShenanigans {
NSMutableArray* blocks = [NSMutableArray array];
NSObject* v = nil;
TOCCancelHandler remover = [self addAndReturnRemoverFor:^{ [v description]; }
to:blocks];
test(blocks.count == 1);
remover();
test(blocks.count == 0); // <--- this test fails
}
-(void(^)(void))addAndReturnRemoverFor:(void(^)(void))block to:(NSMutableArray*)array {
NSLog(@"Argument: %@", block);
[array addObject:block];
NSLog(@"Added___: %@", array.lastObject);
return ^{
NSLog(@"Removing: %@", block);
[array removeObject:block];
};
}
运行此测试时的日志输出是:
Argument: <__NSStackBlock__: 0xbffff220>
Added___: <__NSMallocBlock__: 0x2e283d0>
Removing: <__NSMallocBlock__: 0x2e27ed0>
参数是一个NSStackBlock,存储在堆栈中。为了放在数组或闭包中,必须将它复制到堆中。但是对于数组的添加和闭包的一次这种情况会发生一次。
因此,数组中的NSMallocBlock的地址以83d0结尾,而从数组中删除的闭包中的NSMallocBlock的地址以7ed0结尾。他们是独特的。删除一个并不算除去另一个。
> Bleh,我想我将来需要注意这一点。答案 2 :(得分:0)
当应用程序离开定义块的范围时,必须复制块。一个不好的例子:
BOOL yesno;
dispatch_block_t aBlock;
if (yesno)
{
aBlock = ^(void) { printf ("yesno is true\n");
}
else
{
aBlock = ^(void) { printf ("yesno is false\n");
}
aBlock = [aBlock copy];
已经太晚了!块已经离开了它的范围({bracket}),事情可能会出错。这可以通过不使用{bracket}来解决,但这是您自己调用copy的罕见情况之一。
当您将某个街区存放在某个地方时,99.99%的时间您将离开宣布该街区的范围;通常通过制作块属性来解决这个问题&#34; copy&#34;属性。如果你调用dispatch_async等,则需要复制块,但被调用的函数会这样做。 NSArray和NSDictionary的基于块的迭代器通常不必复制块,因为您仍然在声明块的范围内运行。
[aBlock copy]当块已被复制时没有做任何事情,它只返回块本身。