使用 NSArray、块和手动引用计数导致崩溃所需的说明

Clarifications needed for a crash using NSArray, blocks and Manual Reference Counting

我需要澄清一下我在使用 NSArray、块和手动引用计数时遇到的崩溃。我的目标是将块存储在一个集合中(NSArray 在这种情况下)以便将来重用它们。

我设置了一个小样本来重现这个问题。特别是,我有一个如下所示的 class Item

#import <Foundation/Foundation.h>

typedef void(^MyBlock)();

@interface Item : NSObject

- (instancetype)initWithBlocks:(NSArray*)blocks;

@end

#import "Item.h"

@interface Item ()

@property (nonatomic, strong) NSArray *blocks;

@end

@implementation Item

- (instancetype)initWithBlocks:(NSArray*)blocks
{
    self = [super init];
    if (self) {

        NSMutableArray *temp = [NSMutableArray array];
        for (MyBlock block in blocks) {
            [temp addObject:[[block copy] autorelease]];
        }

        _blocks = [temp copy];            
    }
    return self;
}

用法如下所述(我在应用程序委托中使用)。

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {

    __block typeof(self) weakSelf = self;

    MyBlock myBlock1 = ^() {
        [weakSelf doSomething1];
    };

    MyBlock myBlock2 = ^() {
        [weakSelf doSomething1];
    };

    NSArray *blocks = @[myBlock1, myBlock2];

    // As MartinR suggested the code crashes even
    // if the following line is commented
    Item *item = [[Item alloc] initWithBlocks:blocks];
}

如果我 运行 该应用程序,它会崩溃并显示 EXC_BAD_INSTRUCTION(请注意,我已经启用了 所有异常断点)。特别是,应用程序停止在 main.

int main(int argc, const char * argv[]) {
    return NSApplicationMain(argc, argv);
}

注意:根据 Ken Thomases 的建议,如果您在 llvm 控制台上使用 bt 命令,您将看到回溯。在这种情况下,它显示以下内容:

-[__NSArrayI dealloc]

如果我评论 [weakSelf doSomethingX]; 它可以正常工作而不会崩溃(这并不意味着它是正确的)。

像下面这样稍微修改一下代码,都运行没问题。

// Item does not do anymore the copy/autorelease dance
// since used in the declaration of the blocks
- (instancetype)initWithBlocks:(NSArray*)blocks
{
    self = [super init];
    if (self) {

        _blocks = [blocks retain];

    }
    return self;
}

__block typeof(self) weakSelf = self;

MyBlock myBlock1 = [[^() {
    [weakSelf doSomething1];
} copy] autorelease];

MyBlock myBlock2 = [[^() {
    [weakSelf doSomething1];
} copy] autorelease];

NSArray *blocks = @[myBlock1, myBlock2];

Item *item = [[Item alloc] initWithBlocks:blocks];

这里有什么意义?我想我错过了什么,但我不知道是什么。

更新 1

好的。我将尝试根据@Martin R 和@Ken Thomases 的评论重述我的想法。

默认情况下,如果未向其发送 copy 消息(ARC 为我们完成此操作),则会在堆栈上创建一个块,以便将其移动到堆上。因此,这种情况下的情况如下。我创建了一个 autorelease 数组并添加了两个块,其中 retain 以隐式方式被调用。当 applicationDidFinishLaunching 方法完成执行时,块,因为在堆栈上创建(它们是 automatic 变量)消失。稍后,名为 blocks 的数组将被释放,因为它已被标记为 autorelease。因此,它会崩溃,因为它会将 release 对象发送到不再存在的块。

所以,我的问题如下:将 retain 消息发送到堆栈上的块是什么意思?为什么数组是崩溃的根源(见回溯)? 换句话说,既然一个块在堆栈上,它会增加它的保留计数吗?什么时候超出范围?上瘾了,为什么如果我注释 [weakSelf doSomething1] 行代码可以正常工作?这部分我不是很清楚。

您正在将堆栈中的对象粘贴到自动释放数组中。 BOOM 随之而来。

考虑:

typedef void(^MyBlock)();

int main(int argc, char *argv[]) {
        @autoreleasepool {
          NSObject *o = [NSObject new];
          MyBlock myBlock1 = ^() {
            [o doSomething1];
          };
          NSLog(@"o %p", o);
          NSLog(@"b %p", myBlock1);
          NSLog(@"b retain %p", [myBlock1 retain]);
          NSLog(@"b copy %p", [myBlock1 copy]);
          NSLog(@"s %p", ^{});
          sleep(1000000);
        }
}

Compiled/run as -i386(因为#s更小更明显):

a.out[11729:555819] o 0x7b6510f0
a.out[11729:555819] b 0xbff2dc30
a.out[11729:555819] b retain 0xbff2dc30
a.out[11729:555819] b copy 0x7b6511a0
a.out[11748:572916] s 0x67048

由于对象位于 0x7b,我们可以假设它是堆。 0xb 确实是高端内存,因此也是堆栈。

retain 不会导致 copy(因为这样做总会导致泄漏)并且 retain 在基于堆栈的对象上是没有意义的。


如果你将 [o doSomething1]; 更改为 [nil doSomething1]; 那么它就变成了一个静态块并且存在于只读映射内存中(来自 mach-o 的 TEXT 段的只读可执行页面),因此,没有要释放的分配并且 retain/release/autorelease 是空操作。

如您所见,静态块在 0x67048 左右结束(这个数字可能会从 运行 运行 变化,顺便说一句,由于各种原因。内存不足。

事实上,由于有 sleep(),我们可以 运行 vmmap 对 a.out 进程进行 vmmap 并查看:

==== Writable regions for process 11772
REGION TYPE              START - END     [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
__DATA                 00067000-00068000 [    4K] rw-/rwx SM=ZER  /tmp/a.out

也就是说,静态块位于 mach-o 文件映射可写区域的第一个 4K 段中。请注意,这并不意味着代码位于该可写区域(如果是,则为安全漏洞)。该代码位于映射到可读区域的 TEXT 段中。