在Objective-C中传递块

时间:2012-05-03 02:08:01

标签: objective-c automatic-ref-counting objective-c-blocks

在编写接受块作为参数的方法时,是否需要执行任何特殊操作,例如在执行块之前将块复制到堆中?例如,如果我有以下方法:

- (void)testWithBlock:(void (^)(NSString *))block {
    NSString *testString = @"Test";
    block(testString);
}

在调用它之前,或者在输入方法时,我应该对block执行任何操作吗?或者上面是使用传入块的正确方法?另外,以下调用方法的方法是正确的,还是应该在传递之前对块执行某些操作?

[object testWithBlock:^(NSString *test){
    NSLog(@"[%@]", test);
}];

的地方我需要复制块吗?如果我不使用ARC,这会有什么不同?

4 个答案:

答案 0 :(得分:18)

当您收到一个块作为方法参数时,该块可能是在堆栈上创建的原始块,也可能是复制块(堆上的块)。据我所知,没有办法说出来。因此,一般的经验法则是,如果您要在接收它的方法中执行该块,则不需要复制它。如果您打算将该块传递给另一个方法(可能会立即执行或不立即执行),那么您也不需要复制它(如果打算保留它,接收方法应该复制它)。但是,如果您打算以某种方式存储块以供以后执行,则需要复制它。许多人使用的主要示例是作为实例变量保存的某种完成块:

typedef void (^IDBlock) (id);
@implementation MyClass{
    IDBlock _completionBlock;
}

但是,如果您要将其添加到任何类型的集合类(如NSArray或NSDictionary),您还需要复制它。否则,当您稍后尝试执行该块时,您将收到错误(很可能是EXC_BAD_ACCESS)或可能是数据损坏。

当你执行一个块时,如果块是nil,首先测试是很重要的。 Objective-c允许您将nil传递给块方法参数。如果该块为nil,则在尝试执行时将获得EXC_BAD_ACCESS。幸运的是,这很容易做到。在您的示例中,您要写:

- (void)testWithBlock:(void (^)(NSString *))block {
    NSString *testString = @"Test";
    if (block) block(testString);
}

复制块时需要考虑性能因素。与在堆栈上创建块相比,将块复制到堆中并非易事。它一般不是主要的交易,但如果你反复使用一个块或者迭代地使用一堆块并在每次执行时复制它们,它就会产生性能损失。因此,如果您的方法- (void)testWithBlock:(void (^)(NSString *))block;处于某种循环中,那么复制该块可能会损害您的性能,如果您不需要复制它。

复制块所需的另一个地方是,如果您打算自己调用该块(块递归)。这并不常见,但如果您打算这样做,则必须复制该块。请在此处查看我的问题/答案:Recursive Blocks In Objective-C

最后,如果您要存储块,则需要非常小心创建保留周期。块将保留传递给它的任何对象,如果该对象是实例变量,它将保留实例变量的类(self)。我个人喜欢积木并且一直使用它们。但是,Apple没有为他们的UIKit类使用/存储块,而是坚持使用目标/动作或委托模式。如果你(创建块的类)保留了接收/复制/存储块的类,并且在该块中引用了自己或任何类实例变量,那么你已经创建了一个保留周期(classA - &gt ; classB - > block - > classA)。这非常容易做到,这是我做过很多次的事情。而且,"泄漏"在仪器中没有抓住它。解决这个问题的方法很简单:只需创建一个临时__weak变量(对于ARC)或__block变量(非ARC),该块不会保留该变量。因此,例如,如果'对象'复制/存储块:

[object testWithBlock:^(NSString *test){
    _iVar = test;
    NSLog(@"[%@]", test);
}];

但是,为了解决这个问题(使用ARC):

__weak IVarClass *iVar = _iVar;
[object testWithBlock:^(NSString *test){
    iVar = test;
    NSLog(@"[%@]", test);
}];

您也可以这样做:

__weak ClassOfSelf _self = self;
[object testWithBlock:^(NSString *test){
    _self->_iVar = test;
    NSLog(@"[%@]", test);
}];

请注意,许多人不喜欢上述内容,因为他们认为它很脆弱,但它是一种有效的访问变量的方法。 更新 - 如果您尝试使用' - >'直接访问变量,则当前编译器会发出警告。出于这个原因(以及安全原因),最好为要访问的变量创建属性。因此,您将使用_self->_iVar = test;代替_self.iVar = test;。{{1}}。

更新(更多信息)

通常,最好将接收块的方法视为负责确定是否需要复制块而不是调用者。这是因为接收方法可以是唯一知道块需要保持多长时间或者是否需要复制的方法。您(作为程序员)在编写调用时显然会知道此信息,但如果您在心理上将调用者和接收者视为单独的对象,则调用者会向接收者提供阻止并完成该操作。因此,它不应该知道在块消失后对块做了什么。另一方面,调用者可能已经复制了块(可能它存储了块并且现在将其移交给另一个方法),但接收者(也打算存储块)应该很可能仍然复制块(即使块已经被复制)。接收方不能知道该块已经被复制,并且它接收的一些块可能被复制而其他块可能不被复制。因此,接收器应该始终复制它打算保留的块吗?合理?这基本上是良好的面向对象设计实践。基本上,任何拥有这些信息的人都有责任处理它。

块在Apple的GCD(Grand Central Dispatch)中广泛使用,可轻松实现多线程。通常,在GCD上发送时,您不需要复制块。奇怪的是,这有点违反直觉(如果你考虑的话),因为如果你异步调度一个块,通常创建块的方法将在块执行之前返回,这通常意味着块将过期,因为它是堆栈对象。我不认为GCD将块复制到堆栈中(我在某处读过但又找不到它),相反我认为线程的生命是通过放在另一个线程来扩展的。

Mike Ash有关于块,GCD和ARC的大量文章,您可能会发现它们很有用:

答案 1 :(得分:7)

这一切看起来都不错。但是,您可能需要仔细检查块参数:

@property id myObject;
@property (copy) void (^myBlock)(NSString *);

...

- (void)testWithBlock: (void (^)(NSString *))block
{
    NSString *testString = @"Test";
    if (block)
    {
        block(test);
        myObject = Block_copy(block);
        myBlock = block;
    }
}

...

[object testWithBlock: ^(NSString *test)
{
    NSLog(@"[%@]", test);
}];

应该没问题。我相信他们甚至试图逐步淘汰Block_copy(),但他们还没有。

答案 2 :(得分:4)

正如封锁编程主题指南在“Copying Blocks”下所述:

  

通常,您不需要复制(或保留)块。只有在您希望在销毁块之后使用该块时,才需要制作副本。

在您描述的情况下,您基本上可以将块视为方法的参数,就像它是int或其他基本类型一样。当调用该方法时,将为方法参数分配堆栈上的空间,因此在整个方法执行期间块将存在于堆栈中(就像所有其他参数一样)。当在方法返回时堆栈帧从堆栈顶部弹出时,分配给块的堆栈内存将被释放。因此,在执行方法期间,块保证处于活动状态,因此这里没有内存管理(在ARC和非ARC情况下)。换句话说,你的代码很好。您只需在方法内调用块即可。

正如引用文本所暗示的那样,您需要显式复制块的唯一时间是您希望从创建它的作用域外部访问它(在您的情况下,超出您的堆栈帧的生命周期)方法)。例如,假设您需要一个从Web获取某些数据的方法,并在获取完成后运行一段代码。您的方法签名可能如下所示:

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(void))completionHandler;

由于数据提取是异步发生的,您可能希望保留块(可能在类的属性中),然后在完全提取数据后运行块。在这种情况下,您的实现可能如下所示:

@interface MyClass

@property (nonatomic, copy) void(^dataCompletion)(NSData *);

@end



@implementation MyClass
@synthesize dataCompletion = _dataCompletion;

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler {
    self.dataCompletion = completionHandler;
    [self fetchDataFromURL:url]; 
}

- (void)fetchDataFromURL:(NSURL *)url {
    // Data fetch starts here 
}

- (void)finishedFetchingData:(NSData *)fetchedData {
    // Called when the data is done being fetched
    self.dataCompletion(fetchedData)
    self.dataCompletion = nil; 
}

在此示例中,使用具有copy语义的属性将在块上执行Block_copy()并将其复制到堆中。这发生在行self.dataCompletion = completionHandler中。因此,块从-getDataFromURL:completionHandler:方法的堆栈帧移动到堆,这允许稍后在finishedFetchingData:方法中调用它。在后一种方法中,行self.dataCompletion = nil使属性无效并向存储的块发送Block_release(),从而解除分配。

以这种方式使用属性很好,因为它基本上可以为您处理所有块内存管理(只需确保它是copy(或strong)属性,而不仅仅是{{ 1}})并将在非ARC和ARC情况下工作。如果您想使用原始实例变量来存储块并且在非ARC环境中工作,则必须自己调用retainBlock_copy()Block_retain()如果你想要将块保持在比它作为参数传递的方法的生命周期更长的地方。使用ivar而不是属性编写的上述代码如下所示:

Block_release()

答案 3 :(得分:0)

你知道有两种块:

  1. 存储在堆栈中的块,您明确写为^ {...}的块,并且一旦创建它们的函数就会消失,就像常规堆栈变量一样。当你所属的函数返回后调用堆栈块时,会发生不好的事情。

  2. 堆中的块,复制另一个块时获得的块,与其他对象一样生存的块,就像常规对象一样。

  3. 复制块的唯一原因是当您获得一个块时,或者可能是一个堆栈块(显式本地块^ {...},或者您不知道其来源的方法参数) ,并且您希望将其寿命延长到有限的一个堆栈块中,并且编译器还没有为您完成这项工作。

    想一想:在实例变量中保留一个块。

    在NSArray等集合中添加块。

    这些是在您不确定它已经是堆块时应该复制块的常见示例。

    请注意,当在另一个块中调用块时,编译器会为您执行此操作。