【问题标题】:Why is raising an NSException not bringing down my application?为什么引发 NSException 不会导致我的应用程序崩溃?
【发布时间】:2011-03-21 03:50:26
【问题描述】:

问题

我正在编写一个 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();
}

令人困惑的是,它按预期工作。

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

【问题讨论】:

    标签: cocoa exception multithreading crash raise


    【解决方案1】:

    更新 - 2010 年 11 月 16 日:当 IBAction 方法中引发异常时,此答案存在一些问题。请参阅此答案:

    How can I stop HIToolbox from catching my exceptions?


    这扩展了 David Gelhar 的 答案以及他提供的链接。下面是我如何通过覆盖 NSApplication 的 -reportException: 方法来做到这一点。首先,为 NSApplication 创建一个 ExceptionHandling 类别(仅供参考,您应该在“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...
    }
    

    您可以调用exit(),而不是使用NSApp 的terminate:terminate: 更符合 Cocoa-kosher 标准,但您可能希望跳过 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 子类化,而不是添加一个类别,它也会发生。可能还有其他注意事项。

    【讨论】:

    • 此解决方案过于复杂。下面乔治的回答是正确的做法:“[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];"
    【解决方案2】:

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

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

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

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

    【讨论】:

    • 请注意,只有在 NSApplication 完成调用其所有委托方法之后才会启动。
    • 其实比这更糟糕。它不适用于任何 AppleEvent 处理代码。有关解决方法,请参阅下面的答案。
    【解决方案3】:

    也许您可以使用NSSetUncaughtExceptionHandler,或在NSApplication 上创建一个覆盖-reportException: 的类别,如http://www.cocoadev.com/index.pl?StackTraces 所建议的那样

    【讨论】:

    • 很好的建议,大卫。几个月前我读了很多这个页面,但由于某种原因没有尝试 NSApplication 类别覆盖。我会尝试这样做,因为它比尝试让我的所有代码在后台线程上运行要容易得多!
    【解决方案4】:

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

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

    我从头到尾浏览了有关 NSException 的文档,但没有提及我记得的这一点。我知道这一点的唯一原因是出色的 Cocoa Dev:

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

    解决方案。我猜。

    我有一个几乎完全在主线程上运行的没有 UI 的守护进程。我将不得不转移整个应用程序以运行后台线程,除非其他人可以建议一种停止 NSApplication 仅捕获我抛出的异常的方法。我很确定这是不可能的。

    【讨论】:

    • 我认为您错过了一个页面。 developer.apple.com/mac/library/documentation/Cocoa/Conceptual/… "注意:Cocoa 应用程序主线程上的异常通常不会上升到未捕获异常处理程序的级别,因为全局应用程序对象会捕获所有此类异常。" ...页面的主体还提到了 David Gelhar 谈到的解决方案。
    • 是的,显然我很懒惰阅读。 :) 感谢您指出了这一点。它周围甚至还有一个盒子来突出它。呵呵。
    • 嗨约翰,我在下面发布了一个“答案”,试图更清楚地理解这个问题。有什么想法吗?
    • 没关系,我想我找到了解决问题的方法。我已经相应地更新了我的“答案”。
    【解决方案5】:

    我试图正确理解这一点:为什么 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中调用默认的未捕获异常处理程序:会导致无限循环:前者调用后者

    【讨论】:

      【解决方案6】:

      因此,在您的应用程序委托方法中似乎没有调用异常处理程序的原因是 _NSAppleEventManagerGenericHandler(私有 API)有一个捕获所有异常的 @try @catch 块并在返回errAEEventNotHandled OSErr 之前对它们调用 NSLog。这意味着您不仅会错过应用程序启动中的任何异常,而且基本上会错过处理 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] Please do better exception logging 和雷达 50933868 - NSAppleEventManager should respect exception handling settings
      • 我还应该注意,我上面的修复将改变 AppleEvents 与您的应用程序交互的方式,但仅限于抛出异常的情况。如果没有修复,您的应用程序将返回一个 errAppleEventNotHandled,并将继续尝试跛行,可能处于损坏状态。通过我的修复,应用程序将崩溃,并且无论谁打电话给你都会得到一个 connectionInvalid err。
      猜你喜欢
      • 2021-12-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-26
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      相关资源
      最近更新 更多