ObjC - 如何明确地将所有权移交给将异步执行的块?

ObjC - How to explicitly hand off ownership to a block that will be performed asynchronously?

此问题与 Objective-C 中使用 MRR(非 ARC)和 GCD(Grand Central Dispatch)的 iOS 应用有关。

Concurrency Programming Guide 声明了这一点(强调我的):

For blocks that you plan to perform asynchronously using a dispatch queue, it is safe to capture scalar variables from the parent function or method and use them in the block. However, you should not try to capture large structures or other pointer-based variables that are allocated and deleted by the calling context. By the time your block is executed, the memory referenced by that pointer may be gone. Of course, it is safe to allocate memory (or an object) yourself and explicitly hand off ownership of that memory to the block.

这似乎与 Blocks and Variables doc 相矛盾,其中指出:

When a block is copied, it creates strong references to object variables used within the block.

后面的语句似乎在描述块如何捕获基于指针的变量(对象)。换句话说,该块隐含地取得所有权,而不是父方法显式地将所有权移交给该块。

如何将一个对象的所有权明确地移交给一个块?真的有办法做到这一点吗?在某些情况下是否需要这样做?

这是一个测试。

给定一个名为 Employee 的简单数据 class,具有一些属性:
Employee.h:

#import <Foundation/Foundation.h>    
@interface Employee : NSObject    
@property (nullable, nonatomic, retain) NSNumber* empID;
@property (nullable, nonatomic, retain) NSString* firstName;
@property (nullable, nonatomic, retain) NSString* lastName;    
@end

示例应用调用 dispatchAfterTest 来测试异步调度块中的内存管理。测试一开始要简单得多,但不断扩展以探索不同的可能性。

- (void)dispatchAfterTest {
    NSLog(@"%s started", __func__);

    Employee* employee = [Employee new];
    employee.empID = @(1001);
    employee.firstName = @"First Name";
    employee.lastName = @"Last Name";

    NSMutableArray<Employee*>* employeeMutableArray = [NSMutableArray<Employee*> new];
    [employeeMutableArray addObject:employee];
    [employee release];
    employee = [Employee new];
    employee.empID = @(1002);
    employee.firstName = @"Adam";
    employee.lastName = @"Zam";
    [employeeMutableArray addObject:employee];

    Employee* employee3 = [Employee new];
    employee3.empID = @(1003);
    employee3.firstName = @"John";
    employee3.lastName = @"Kealson";
    [employeeMutableArray addObject:employee3];

    NSArray<Employee*>* employeeArray = [[NSArray<Employee*> alloc] initWithArray:employeeMutableArray];
    NSArray<Employee*>* autoreleasedArray = [NSArray<Employee*> arrayWithArray:employeeMutableArray];

    // dispatch a block asynchronously that will use the employee object, employeeMutableArray, and employeeArray
    NSLog(@"%s calling dispatch_after()", __func__);
    //dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    double delayInSeconds = 5;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(popTime, queue, ^(void){
        NSLog(@"%s start of dispatch_after block", __func__);
        NSLog(@"%s empID=%@, firstName=%@, lastName=%@", __func__, employee.empID, employee.firstName, employee.lastName); // Expecting EXC_BAD_ACCESS error if employee object has been deallocated.
        NSLog(@"%s employeeMutableArray:", __func__);
        for (Employee* emp in employeeMutableArray) {
            NSLog(@"%s   ID: %@  Name: %@ %@", __func__, emp.empID, emp.firstName, emp.lastName);
        }
        NSLog(@"%s employeeArray:", __func__);
        for (Employee* emp in employeeArray) {
            NSLog(@"%s   ID: %@  Name: %@ %@", __func__, emp.empID, emp.firstName, emp.lastName);
        }
        NSLog(@"%s autoreleasedArray:", __func__);

        for (Employee* emp in autoreleasedArray) {
            NSLog(@"%s   ID: %@  Name: %@ %@", __func__, emp.empID, emp.firstName, emp.lastName);
        }

        employee.lastName = @"Zammal";
        NSLog(@"%s employee.lastName changed to %@", __func__, employee.lastName);
        NSLog(@"%s employeeMutableArray[1].lastName=%@", __func__, employeeMutableArray[1].lastName);
        NSLog(@"%s        employeeArray[1].lastName=%@", __func__, employeeArray[1].lastName);
        NSLog(@"%s    autoreleasedArray[1].lastName=%@", __func__, autoreleasedArray[1].lastName);

        NSLog(@"%s end of dispatch_after block", __func__);
    });

    NSLog(@"%s changing 1st employee's name to John Smith", __func__);
    employeeMutableArray[0].firstName = @"John";
    employeeMutableArray[0].lastName = @"Smith";

    //NSLog(@"%s releasing serial dispatch queue", __func__);
    //dispatch_release(queue); // Not needed for global dispatch queues, including the concurrent dispatch queues or the main dispatch queue.

    NSLog(@"%s releasing employee objects and arrays (except autoreleasedArray)", __func__);
    [employee release];
    [employee3 release];
    [employeeMutableArray release];
    [employeeArray release];
    /*
    for (int ii = 0; ii < 10; ii++) {
        // The following NSLog() throws an EXC_BAD_ACCESS runtime error after the dispatch_after block finishes.
        NSLog(@"%s accessing employee object after release:  ID=%@, firstName=%@, lastName=%@", __func__, employee.empID, employee.firstName, employee.lastName);
        NSLog(@"%s sleeping 1 seconds", __func__);
        [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    }
    */

    NSLog(@"%s finished", __func__);
}

这是输出:

2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] started
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] calling dispatch_after()
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] changing 1st employee's name to John Smith
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] releasing serial dispatch queue
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] releasing employee objects and arrays (except autoreleasedArray)
2016-03-06 14:49:56.127 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController dispatchAfterTest] finished
2016-03-06 14:49:56.139 StudyObjC_MRR_CoreData[917:184234] -[ContactsTableViewController tableView:viewForHeaderInSection:]
2016-03-06 14:50:01.619 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke start of dispatch_after block
2016-03-06 14:50:01.620 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke empID=1002, firstName=Adam, lastName=Zam
2016-03-06 14:50:01.620 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeMutableArray:
2016-03-06 14:50:01.620 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1001  Name: John Smith
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1002  Name: Adam Zam
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1003  Name: John Kealson
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeArray:
2016-03-06 14:50:01.621 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1001  Name: John Smith
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1002  Name: Adam Zam
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1003  Name: John Kealson
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke autoreleasedArray:
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1001  Name: John Smith
2016-03-06 14:50:01.622 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1002  Name: Adam Zam
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke   ID: 1003  Name: John Kealson
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employee.lastName changed to Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke employeeMutableArray[1].lastName=Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke        employeeArray[1].lastName=Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke    autoreleasedArray[1].lastName=Zammal
2016-03-06 14:50:01.623 StudyObjC_MRR_CoreData[917:184289] __48-[ContactsTableViewController dispatchAfterTest]_block_invoke end of dispatch_after block

一些注意事项。

在测试的早期,当我认为该块试图访问自动释放的 employeeArray(即通过 [NSArray arrayWithArray:] 创建)时,该块抛出了 NSInvalidArgumentException。我无法复制该异常,不幸的是,我没有当时的测试代码副本。

异常详情如下:

2016-02-28 10:21:48.235 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke start of dispatch_after block
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke empID=1002, firstName=Adam, lastName=Zam
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke employeeMutableArray:
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke   ID: 1001  Name: First Name Last Name
2016-02-28 10:21:48.236 StudyObjC_MRR_CoreData[669:48826] __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke   ID: 1002  Name: Adam Zam
2016-02-28 10:21:48.237 StudyObjC_MRR_CoreData[669:48826] -[CALayer countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x7f80b8f3d390
2016-02-28 10:21:48.241 StudyObjC_MRR_CoreData[669:48826] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CALayer countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x7f80b8f3d390'
First throw call stack:
(
0   CoreFoundation                      0x00000001040d0e65 __exceptionPreprocess + 165
1   libobjc.A.dylib                     0x00000001037c0deb objc_exception_throw + 48
2   CoreFoundation                      0x00000001040d948d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3   CoreFoundation                      0x000000010402690a ___forwarding___ + 970
4   CoreFoundation                      0x00000001040264b8 _CF_forwarding_prep_0 + 120
5   StudyObjC_MRR_CoreData              0x00000001032bcc5b __54-[ContactsTableViewController dispatchAsyncExperiment]_block_invoke + 955

我的第一个猜测是该块没有保留数组,因为它是一个 "autoreleased" 对象,所以我将其更改为使用 initWithArray: 初始化并在 dispatch_after 之后手动释放它() 称呼。

稍后添加了 autoreleasedArray 以尝试复制异常,但它工作正常。

重述问题(如果我可以将一个问题变成多个):

  1. 测试方法是否使用安全的方式将局部对象变量传递给将异步执行的块,或者所有权是否需要按照并发指南所述显式移交给块?
  2. 如果所有权需要明确移交给区块,那是怎么做到的?
  3. [奖金问题]如果当前方法不安全,如何修改它以证明它不安全(即导致释放问题),同时基本上仍然使用相同的方法(隐式捕获局部对象变量由街区)?

Is the test method using a safe way to pass local object variables to a block that will be executed asynchronously, or does ownership need to be handed off explicitly to the block as the Concurrency guide states?

您正在测试指南未提及的内容。它正在谈论 非对象 分配,例如 malloc 数组。该块将获取 指针 的副本,但无法对指向的分配做任何事情。对于这样的分配,块的最终责任是释放内存。

(括号中的“(or an object)”我不是很确定,但我的阅读是它基本上放错了地方。这句话的意思似乎是"You can allocate memory and give the Block ownership; you can also allocate objects"。这可能是一个微小的文档错误。)

使用NSObject类型是绝对安全的。您已经引用了 documented Block behavior towards objects:

When a block is copied, it creates strong references to object variables used within the block.

(请注意并发指南在您的引述之后说:"Dispatch queues copy blocks that are added to them"。)

bbum makes this somewhat more explicit:

For captured values of type id — for Objective-C object pointers — the object is retained when the block is created [emphasis mine] (when execution passes over the block) and then released when the block is destroyed. This ensures that any “captured” objects survive as long as the block does.

(即使没有用 ARC 编译,)编译器显然在做正确的事情:如果你在 Employee 上重写 dealloc,你会看到它不会在实例上调用,直到块的末尾。 (当您不使用 ARC 进行编译时,您可以通过覆盖 class 上的那些方法来观看 retain/release/dealloc 事件。)

真的,没有其他方法可以让 Blocks 有效地发挥作用(尽管这样想也不是没有道理的)。编译器处理这个的替代方法是一大堆样板文件:

Employee * bEmployee = [employee retain];
dispatch_async(q, ^{
      // use bEmployee
      [bEmployee release];
  });

对于您想在块内使用的每个对象(或至少将 retain/release 显式发送到现有指针)。


关于 Blocks 中的对象和调试的进一步说明:如果这不安全,打开 NSZombie 调试功能将是最好的确认方式。通常,Cocoa 实际上不会覆盖或销毁已释放的对象,它们仍然位于内存中,直到该内存被实际重新用于其他用途。 Ref. 您偶尔可以发送悬空指针而不是僵尸。