EXC_BAD_ACCESS调用块时出错

3

更新 | 我已经上传了一个使用面板并崩溃的示例项目: http://w3style.co.uk/~d11wtq/BlocksCrash.tar.gz (我知道“选择...”按钮什么也不做,我还没有实现它)。

更新2 | 刚刚发现我甚至不需要在newFilePanel上调用任何东西就能导致崩溃,我只需要在语句中使用它。

这也会导致崩溃:

[newFilePanel beginSheetModalForWindow:[windowController window] completionHandler:^(NSInteger result) {
    newFilePanel; // Do nothing, just use the variable in an expression
}];

看起来控制台打印的最后一条信息有时是这样的:"无法反汇编dyld_stub_objc_msgSend_stret。",有时是这样的:"无法访问地址0xa的内存。"

我创建了自己的面板(NSPanel子类),试图提供类似于NSOpenPanel / NSSavePanel的API,通过呈现自己作为面板并在完成时调用块。

这是接口:

//
//  EDNewFilePanel.h
//  MojiBaker
//
//  Created by Chris Corbyn on 29/12/10.
//  Copyright 2010 Chris Corbyn. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@class EDNewFilePanel;

@interface EDNewFilePanel : NSPanel <NSTextFieldDelegate> {
    BOOL allowsRelativePaths;

    NSTextField *filenameInput;

    NSButton *relativePathSwitch;

    NSTextField *localPathLabel;
    NSTextField *localPathInput;
    NSButton *chooseButton;

    NSButton *createButton;
    NSButton *cancelButton;
}

@property (nonatomic) BOOL allowsRelativePaths;

+(EDNewFilePanel *)newFilePanel;

-(void)beginSheetModalForWindow:(NSWindow *)aWindow completionHandler:(void (^)(NSInteger result))handler;
-(void)setFileName:(NSString *)fileName;
-(NSString *)fileName;
-(void)setLocalPath:(NSString *)localPath;
-(NSString *)localPath;
-(BOOL)isRelative;

@end

实现中的关键方法:

-(void)beginSheetModalForWindow:(NSWindow *)aWindow completionHandler:(void (^)(NSInteger result))handler {
    [NSApp beginSheet:self
       modalForWindow:aWindow
        modalDelegate:self
       didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
          contextInfo:(void *)[handler retain]];
}

-(void)dismissSheet:(id)sender {
    [NSApp endSheet:self returnCode:([sender tag] == 1) ? NSOKButton : NSCancelButton];
}

-(void)sheetDidEnd:(NSWindow *)aSheet returnCode:(NSInteger)result contextInfo:(void *)contextInfo {
    ((void (^)(NSUInteger result))contextInfo)(result);
    [self orderOut:self];
    [(void (^)(NSUInteger result))contextInfo release];
}

只要我的块是一个没有任何操作和空主体的无操作,则所有这些都可以工作。当表格被关闭时,我的块被调用。
EDNewFilePanel *newFilePanel = [EDNewFilePanel newFilePanel];
[newFilePanel setAllowsRelativePaths:[self hasSelectedItems]];
[newFilePanel setLocalPath:@"~/"];
[newFilePanel beginSheetModalForWindow:[windowController window] completionHandler:^(NSInteger result) {
    NSLog(@"I got invoked!");
}];

但是当我试图从块内部访问面板时,就会崩溃并出现EXC_BAD_ACCESS错误。例如,下面的代码就会导致崩溃:

EDNewFilePanel *newFilePanel = [EDNewFilePanel newFilePanel];
[newFilePanel setAllowsRelativePaths:[self hasSelectedItems]];
[newFilePanel setLocalPath:@"~/"];
[newFilePanel beginSheetModalForWindow:[windowController window] completionHandler:^(NSInteger result) {
    NSLog(@"I got invoked and the panel is %@!", newFilePanel);
}];

从调试器中不清楚原因。栈中的第一项(零0)只是显示“??”,没有列出任何内容。

栈中的下一项(1和2)是对-endSheet:returnCode:-dismissSheet:的调用。在调试器中查看变量,没有异常/超出范围的情况。

我曾经认为可能是面板已被释放掉(因为它是自动释放的),但即使在创建后立即调用-retain也无济于事。

我是否实现有误?

2个回答

12

当你在一个方法中使用retain保留参数,而后在另一个方法中使用release释放该对象时,这似乎有点奇怪,尤其是当该对象不是实例变量的情况下。

我建议将你的completionHandler部分设置为实例变量。毕竟你一次也只能显示一个sheet,这样做会更简洁。

此外,你的EXC_BAD_ACCESS很可能来自于beginSheet:方法中[handler retain]的调用。你可能使用类似以下的东西调用此方法(为了简洁起见):

[myObject doThingWithCompletionHandler:^{ NSLog(@"done!"); }];
如果是这种情况,你必须将代码块进行复制(而不是保留)。如上所述,代码块存在于堆栈中。但是,如果该堆栈帧被弹出执行堆栈,那么该代码块也会消失。poof 任何后续尝试访问此代码块都会导致崩溃,因为您正在尝试执行不存在的代码,并已被垃圾替换。因此,您必须在代码块上调用copy以将其移动到堆中,这样它可以存在于创建它的堆栈帧的生命周期之外。

太棒了,谢谢!复制解决一切。另外,感谢您对我保留/释放位置的建议。即使对我来说也感觉很奇怪,但我记得在苹果文档中有一个模式(他们正在保留和释放NSNumber)。瞬态ivar可能更少风险。 - d11wtq
忘记补充了,你的回答非常清晰,很有道理,因为面板是异步调用的,堆栈当然会结束,所以必须复制它以保留它。 - d11wtq
我建议还使用 Block_copy 和 Block_release。 - the Reverend

-1
尝试使用__block修饰符定义您的EDNewFilePanel:
__block EDNewFilePanel *newFilePanel = [EDNewFilePanel newFilePanel];

这样做可以在调用块之后保留对象,即使面板对象已被释放。作为一个不相关的副作用,这也将使它在块范围内可变。


尝试过了,但没有任何改变,它仍然会崩溃。顺便提一下,NSOpenPanel不需要使用__block。感谢你的帮助(附注:我已在问题中添加了一个示例项目的链接)。 - d11wtq
2
__block 正好与您所描述的相反。 - bbum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接