【问题标题】:Clarifications needed for a crash using NSArray, blocks and Manual Reference Counting使用 NSArray、块和手动引用计数进行崩溃所需的说明
【发布时间】:2015-02-16 20:03:37
【问题描述】:

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

我设置了一个小样本来复制该问题。特别是,我有一个类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 而崩溃(请注意,我已经启用了 All Exceptions 断点)。特别是,应用程序在 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 的 cmets 回顾我的想法。

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

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

【问题讨论】:

  • 我认为问题出在NSArray *blocks = @[myBlock1, myBlock2];。与您的 Item 类一样,添加到集合时必须复制块:NSArray *blocks = @[[[myBlock1 copy] autorelease], [[myBlock2 copy] autorelease]];
  • Xcode 可能正在折叠堆栈跟踪。确保将堆栈跟踪下方的滑块一直拖到右侧。此外,当 Xcode 让您的程序在崩溃时停止时,您可以在控制台视图的“lldb”提示符处键入“bt”。
  • 马丁可能是对的。数组字面量@[ ... ] 有效地创建了一个自动释放的数组。崩溃可能发生在最终释放时,即在定义块的范围之后,因此在它们被释放之后。 -release 在那个时候被发送到垃圾对象。您可以尝试使用 +alloc-initWithObjects: 创建该数组,然后显式释放它,但这并不能保证它不会存在于自动释放池中。
  • 即使省略Item *item = ... 语句也会崩溃,因此问题与Item 类无关。回溯显示-[__NSArrayI dealloc],这可能是blocksarray 的释放(正如Ken 在最后一条评论中所怀疑的那样)。
  • 我怀疑 -retain-release 或仍然在堆栈上的块上没有操作,因为它们不可能影响对象的实际生命周期。类似于将它们发送到具有静态生命周期的字符串文字 (@"...")。

标签: objective-c memory-management objective-c-blocks


【解决方案1】:

您正在将堆栈中的对象粘贴到自动释放的数组中。 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);
        }
}

编译/运行为-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 段的只读可执行页面),因此没有分配释放并且保留/释放/自动释放是无操作的。

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

实际上,因为有了 sleep(),我们可以针对 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 段中。

【讨论】:

  • 非常感谢您的解释。非常感谢。当你说 如果你改变了 [o doSomething1];到 [nil doSomething1];那么这就变成了一个静态块,你说的静态块是什么意思?您是否将其用作通用术语来强调该块未从外部范围捕获任何引用?谢谢;)
  • @flexaddicted 静态块就像静态数据。与在代码中使用 @"foobar" 相同;如果编译器确定某些东西不能、永远不会在运行时改变,那么它通常会将所有静态信息布局到可执行文件内的一些内存页中,这些内存页被标记为看起来像 Objective-C 类,但是永远不会分配/释放,而只是从可执行文件映射。这显然很快。且高效。最常见的例子是NSCFConstantString。实例是NSString 的子类。
  • @flexaddicted 哦——编译器版本、目标架构和/或优化级别可能会微妙地改变行为。
猜你喜欢
  • 2021-09-07
  • 1970-01-01
  • 1970-01-01
  • 2015-12-21
  • 2019-10-20
  • 1970-01-01
  • 2012-04-06
  • 2016-06-16
  • 1970-01-01
相关资源
最近更新 更多