为什么抛出NSException不会导致我的应用程序崩溃?

12

问题

我正在编写一个Cocoa应用程序,希望抛出能够使应用程序嘈杂地崩溃的异常。

我在应用程序代理中有以下几行:

[NSException raise:NSInternalInconsistencyException format:@"This should crash the application."];
abort();

问题是,它们不会使应用程序停止-消息只会被记录到控制台,并且应用程序会继续执行。

据我所知,异常的全部意义在于它们在异常情况下被触发。在这些情况下,我希望应用程序以明显的方式退出。但这并没有发生。

我尝试过的

我已经尝试了:

-(void)applicationDidFinishLaunching:(NSNotification *)note
    // ...
    [self performSelectorOnMainThread:@selector(crash) withObject:nil waitUntilDone:YES];
}

-(void)crash {
    [NSException raise:NSInternalInconsistencyException format:@"This should crash the application."];
    abort();
}

这个方法不起作用

-(void)applicationDidFinishLaunching:(NSNotification *)note
    // ...
    [self performSelectorInBackground:@selector(crash) withObject:nil];
}

-(void)crash {
    [NSException raise:NSInternalInconsistencyException format:@"This should crash the application."];
    abort();
}

这有点令人困惑,但实际上它按预期工作。

发生了什么?我做错了什么吗?

6个回答

10

更新 - Nov 16, 2010: 当在IBAction方法中抛出异常时,此答案存在一些问题。请查看以下答案:

如何阻止HIToolbox捕获我的异常?


以下是我通过覆盖NSApplication的-reportException:方法来完成的,这扩展了David Gelhar的答案和他提供的链接。首先,为NSApplication创建一个ExceptionHandling类别(FYI,您应该在“ExceptionHandling”之前添加一个2-3个字母的缩写以减少名称冲突的风险):

NSApplication+ExceptionHandling.h

#import <Cocoa/Cocoa.h>

@interface NSApplication (ExceptionHandling)

- (void)reportException:(NSException *)anException;

@end

NSApplication+ExceptionHandling.m

#import "NSApplication+ExceptionHandling.h"

@implementation NSApplication (ExceptionHandling)

- (void)reportException:(NSException *)anException
{
    (*NSGetUncaughtExceptionHandler())(anException);
}

@end
第二步,我在NSApplication的代理中执行以下操作:
AppDelegate.m
void exceptionHandler(NSException *anException)
{
    NSLog(@"%@", [anException reason]);
    NSLog(@"%@", [anException userInfo]);

    [NSApp terminate:nil];  // you can call exit() instead if desired
}

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
    NSSetUncaughtExceptionHandler(&exceptionHandler);

    // additional code...

    // NOTE: See the "UPDATE" at the end of this post regarding a possible glitch here...
}

与其使用NSApp的terminate:方法,您可以改为调用exit()。虽然terminate:更符合Cocoa规范,但如果出现异常并且您想直接崩溃,可以跳过applicationShouldTerminate:代码,改用exit()

#import "sysexits.h"

// ...

exit(EX_SOFTWARE);
每当在主线程上抛出异常并且未被捕获和销毁时,您的自定义未捕获异常处理程序将被调用,而不是NSApplication的处理程序。这使您可以使应用程序崩溃,以及其他操作。

更新:

上述代码中似乎存在一个小错误。只有在NSApplication完成调用其所有委托方法后,您的自定义异常处理程序才会开始运行。这意味着如果您在applicationWillFinishLaunching:applicationDidFinishLaunching:awakeFromNib:中执行一些设置代码,则默认的NSApplication异常处理程序似乎仍然有效,直到完全初始化为止。

这意味着如果您执行以下操作:

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
        NSSetUncaughtExceptionHandler(&exceptionHandler);

        MyClass *myClass = [[MyClass alloc] init];   // throws an exception during init...
}

你的exceptionHandler不会获取异常。NSApplication会获取异常并将其记录。

要修复此问题,只需将任何初始化代码放入@try/@catch/@finally块中,并可以调用自定义的exceptionHandler

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
    NSSetUncaughtExceptionHandler(&exceptionHandler);

    @try
    {
        MyClass *myClass = [[MyClass alloc] init];   // throws an exception during init...
    }
    @catch (NSException * e)
    {
        exceptionHandler(e);
    }
    @finally
    {
        // cleanup code...
    }
}
现在您的exceptionHandler()会获取异常并可以相应地处理它。在NSApplication完成调用所有委托方法之后,NSApplication+ExceptionHandling.h分类启动,通过其自定义的-reportException:方法调用exceptionHandler()。此时,当您希望将异常上升到未捕获的异常处理程序时,无需担心@try/@catch/@finally。
我对导致这种情况的原因有点困惑。可能是API中的某些幕后操作。即使我子类化NSApplication,而不是添加类别,它也会发生。还可能存在其他注意事项。

1
这个解决方案过于复杂。George在下面的回答中提供了正确的方法:“[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];” - corbin dunn

8

有一个非常简单的解决方案:

[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];

如果您使用@try ...@catch,它不会崩溃您的应用程序。

我无法想象为什么这不是默认设置。


请注意,直到NSApplication完成调用其所有委托方法后,该操作才会生效。 - dmaclach
实际上情况比这更糟。它在任何AppleEvent处理代码中都无法工作。请参见下面的答案以获取解决方法。 - dmaclach

3

很好的建议,David。几个月前我读过这个页面很多次,但由于某些原因没有尝试NSApplication类别覆盖。我会尝试用这种方式来做,因为它比尝试让所有代码在后台线程上运行要容易得多! - John Gallagher

2
我已经发布了这个问题和答案,因为我希望有人在大约一年前告诉我这个:

主线程抛出的异常会被NSApplication捕获。

我浏览了关于NSException的文档,但没有想起看到这方面的提及。之所以我知道这一点,是因为有了出色的Cocoa Dev:

http://www.cocoadev.com/index.pl?ExceptionHandling

解决方案。我猜。
我有一个没有 UI 的守护进程,几乎完全在主线程上运行。除非有人能建议一种停止 NSApplication 捕获我抛出的异常的方法,否则我将不得不将整个应用程序转移到后台线程中运行。我相当确定这是不可能的。

4
我认为您错过了一页。http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Exceptions/Concepts/UncaughtExceptions.html#//apple_ref/doc/uid/20000056-BAJDDGGD“注意:Cocoa应用程序的主线程上的异常通常不会上升到未捕获异常处理程序的级别,因为全局应用程序对象会捕获所有这些异常。”...页面的正文还提到了David Gelhar提出的解决方案。 - Joshua Nozzi
是的,显然我非常懒,没有好好阅读。 :) 谢谢你指出来。甚至还有一个框框把它圈起来突出显示。呆。 - John Gallagher
嗨John,我在下面发布了一个“答案”,试图更清楚地理解这个问题。有什么想法吗? - Enchilada
没关系,我想我已经找到了解决我的问题的方法。我已经相应地更新了我的“答案”。 - Enchilada

1
我正在努力理解这个问题:为什么在NSApplication的以下类别方法中会导致无限循环?在那个无限循环中,“引发了一个未捕获的异常”被无限次记录:
- (void)reportException:(NSException *)anException
{
    // handle the exception properly
    (*NSGetUncaughtExceptionHandler())(anException);
}

为了测试(和理解目的),这是我所做的唯一事情,即创建上述类别方法。(根据http://www.cocoadev.com/index.pl?StackTraces中的说明)

为什么会导致无限循环?这与默认未捕获异常处理程序方法应该执行的操作不一致,即仅记录异常并退出程序。(请参见http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Exceptions/Concepts/UncaughtExceptions.html#//apple_ref/doc/uid/20000056-BAJDDGGD

难道默认的未捕获异常处理程序实际上是再次抛出异常,导致这个无限循环吗?

注意:我知道只创建这个类别方法很傻。这样做的目的是为了更好地理解。

更新:算了,我现在明白了。这是我的理解。默认情况下,我们知道NSApplication的reportException:方法记录异常。但是,根据文档,未捕获的异常处理程序会记录异常并退出程序。然而,为了更精确,文档应该这样表述:默认的未捕获异常处理程序调用NSApplication的reportException:方法(以记录它,该方法的默认实现确实如此),然后退出程序。所以,现在应该清楚为什么在重写reportException:内部调用默认的未捕获异常处理程序会导致无限循环:前者调用后者


1
因此,事实证明,在您的应用程序委托方法中似乎异常处理程序不会被调用的原因是 _NSAppleEventManagerGenericHandler(私有 API)具有一个 @try @catch 块,它捕获所有异常并仅在返回时调用 NSLog 并使用 errAEEventNotHandled OSErr。这意味着您不仅会错过任何应用程序启动中的异常,而且基本上会错过任何处理 AppleEvent 时发生的异常,包括(但不限于)打开文档、打印、退出和任何 AppleScript。
所以,我的“解决方案”:
#import <Foundation/Foundation.h>
#include <objc/runtime.h>

@interface NSAppleEventManager (GTMExceptionHandler)
@end

@implementation NSAppleEventManager (GTMExceptionHandler)
+ (void)load {
  // Magic Keyword for turning on crashes on Exceptions
  [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];

  // Default AppleEventManager wraps all AppleEvent calls in a @try/@catch
  // block and just logs the exception. We replace the caller with a version
  // that calls through to the NSUncaughtExceptionHandler if set.
  NSAppleEventManager *mgr = [NSAppleEventManager sharedAppleEventManager];
  Class class = [mgr class];
  Method originalMethod = class_getInstanceMethod(class, @selector(dispatchRawAppleEvent:withRawReply:handlerRefCon:));
  Method swizzledMethod = class_getInstanceMethod(class, @selector(gtm_dispatchRawAppleEvent:withRawReply:handlerRefCon:));
  method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (OSErr)gtm_dispatchRawAppleEvent:(const AppleEvent *)theAppleEvent
                      withRawReply:(AppleEvent *)theReply
                     handlerRefCon:(SRefCon)handlerRefCon {
  OSErr err;
  @try {
    err = [self gtm_dispatchRawAppleEvent:theAppleEvent withRawReply:theReply handlerRefCon:handlerRefCon];
  } @catch(NSException *exception) {
    NSUncaughtExceptionHandler *handler = NSGetUncaughtExceptionHandler();
    if (handler) {
      handler(exception);
    }
    @throw;
  }
  @catch(...) {
    @throw;
  }
  return err;
}
@end

有趣的额外说明:NSLog(@"%@", exception) 等同于 NSLog(@"%@", exception.reason)NSLog(@"%@", [exception debugDescription]) 将给出原因以及完全符号化的堆栈回溯。

_NSAppleEventManagerGenericHandler 中的默认版本只调用 NSLog(@"%@", exception)(macOS 10.14.4 (18E226))。


已提交雷达报告50933952 - [NSAppleEventManager] 请改进异常日志记录 和雷达报告50933868 - NSAppleEventManager 应该尊重异常处理设置 - dmaclach
我还应该注意,我上面的修复将改变AppleEvents与您的应用程序交互的方式,但仅在抛出异常的情况下。没有这个修复,您的应用程序将返回errAppleEventNotHandled,并将继续尝试挣扎,可能处于损坏状态。有了我的修复,应用程序将崩溃,调用您的人将收到connectionInvalid err。 - dmaclach

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