为什么不@try ... @ catch使用 - [NSFileHandle writeData]?

mac*_*rth 6 cocoa objective-c

我有一个类似于tee实用程序的方法.它接收通知已在管道上读取数据,然后将该数据写入一个或多个管道(连接到从属应用程序).如果从属应用程序崩溃,那么该管道坏了,我自然会得到一个异常,然后在@try ... @ catch块中处理.

这大部分时间都有效.令我困惑的是偶尔,异常会因未捕获的异常而崩溃应用程序,并指向writeData行.当它崩溃时我无法弄清楚模式是什么,但为什么它不会被抓住?(注意,这不是在调试器内部执行.)

这是代码:

//in setup:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tee:) name:NSFileHandleReadCompletionNotification object:fileHandle];

 -(void)tee:(NSNotification *)notification
{
//    NSLog(@"Got read for tee ");

NSData *readData = notification.userInfo[NSFileHandleNotificationDataItem];
totalDataRead += readData.length;
//    NSLog(@"Total Data Read %ld",totalDataRead);
NSArray *pipes = [teeBranches objectForKey:notification.object];

if (readData.length) {
    for (NSPipe *pipe in pipes {
           @try {
                [[pipe fileHandleForWriting] writeData:readData];
            }
            @catch (NSException *exception) {
                NSLog(@"download write fileHandleForWriting fail: %@", exception.reason);
                if (!_download.isCanceled) {
                    [_download rescheduleOnMain];
                    NSLog(@"Rescheduling");
                }
                return; 
            }
            @finally {
            }
    }
 }
Run Code Online (Sandbox Code Playgroud)

我应该提一下,我在AppDelegate> appDidFinishLaunching中设置了一个信号处理程序:

signal(SIGPIPE, &signalHandler);
signal(SIGABRT, &signalHandler );

void signalHandler(int signal)
{
    NSLog(@"Got signal %d",signal);
}
Run Code Online (Sandbox Code Playgroud)

无论应用程序崩溃还是信号被捕获,都会执行.这是一个崩溃回溯示例:

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000

Application Specific Information:
*** Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '*** -[NSConcreteFileHandle writeData:]: Broken pipe'
abort() called
terminating with uncaught exception of type NSException

Application Specific Backtrace 1:
0   CoreFoundation                      0x00007fff838cbbec __exceptionPreprocess + 172
1   libobjc.A.dylib                     0x00007fff90e046de objc_exception_throw + 43
2   CoreFoundation                      0x00007fff838cba9d +[NSException raise:format:] + 205
3   Foundation                          0x00007fff90a2be3c __34-[NSConcreteFileHandle writeData:]_block_invoke + 81
4   Foundation                          0x00007fff90c53c17 __49-[_NSDispatchData enumerateByteRangesUsingBlock:]_block_invoke + 32
5   libdispatch.dylib                   0x00007fff90fdfb76 _dispatch_client_callout3 + 9
6   libdispatch.dylib                   0x00007fff90fdfafa _dispatch_data_apply + 110
7   libdispatch.dylib                   0x00007fff90fe9e73 dispatch_data_apply + 31
8   Foundation                          0x00007fff90c53bf0 -[_NSDispatchData enumerateByteRangesUsingBlock:] + 83
9   Foundation                          0x00007fff90a2bde0 -[NSConcreteFileHandle writeData:] + 150
10  myApp                               0x000000010926473e -[MTTaskChain tee:] + 2030
11  CoreFoundation                      0x00007fff838880dc __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
12  CoreFoundation                      0x00007fff83779634 _CFXNotificationPost + 3140
13  Foundation                          0x00007fff909bb9b1 -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
14  Foundation                          0x00007fff90aaf8e6 _performFileHandleSource + 1622
15  CoreFoundation                      0x00007fff837e9ae1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
16  CoreFoundation                      0x00007fff837dbd3c __CFRunLoopDoSources0 + 476
17  CoreFoundation                      0x00007fff837db29f __CFRunLoopRun + 927
18  CoreFoundation                      0x00007fff837dacb8 CFRunLoopRunSpecific + 296
19  HIToolbox                           0x00007fff90664dbf RunCurrentEventLoopInMode + 235
20  HIToolbox                           0x00007fff90664b3a ReceiveNextEventCommon + 431
21  HIToolbox                           0x00007fff9066497b _BlockUntilNextEventMatchingListInModeWithFilter + 71
22  AppKit                              0x00007fff8acf5cf5 _DPSNextEvent + 1000
23  AppKit                              0x00007fff8acf5480 -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] + 194
24  AppKit                              0x00007fff8ace9433 -[NSApplication run] + 594
25  AppKit                              0x00007fff8acd4834 NSApplicationMain + 1832
26  myApp                               0x00000001091b16a2 main + 34
27  myApp                               0x00000001091ab864 start + 52
Run Code Online (Sandbox Code Playgroud)

mac*_*rth 4

所以,Crashlytics 的好心人能够在这里帮助我。引用他们的话:

\n\n
\n

故事是这样的:

\n\n
    \n
  • 管道因子进程崩溃而终止。下一次读/写将导致错误。
  • \n
  • 该写入发生,会导致 SIGPIPE(不是运行时异常)。
  • \n
  • 如果该 SIGPIPE 被屏蔽/忽略,NSFileHandle 会检查 errno 并创建一个抛出的运行时异常。
  • \n
  • 比你的 tee更深的函数:方法已将此写入包装在 @try/@catch 中(通过在__cxa_begin_catch)\n \n
      \n
    • 该函数原来是“_dispatch_client_callout”,它调用 objc_terminate,从而有效地终止进程。
    • \n
  • \n
\n\n

为什么_dispatch_client_callout要这样做?我不确定,但您可以在此处查看代码:\n \xef\xbb\xbf http://www.opensource.apple.com/source/libdispatch/libdispatch-228.23/src/object.m

\n\n

不幸的是,AppKit 在面对运行时异常时表现不佳。

\n\n

因此,您是对的,NSFileHandle 引发了有关管道死亡的运行时异常,但不是在发出杀死进程的信号之前。其他人也遇到过这个确切的问题(在 iOS 上,它对运行时异常有更好的语义)。

\n\n

\xef\xbb\xbf如何在 NSFIleHandle 处理中捕获 EPIPE?

\n\n

简而言之,我不相信你能够捕获这个异常。但是,通过忽略 SIGPIPE使用较低级别的 API 来读/写此文件句柄,我相信您可以解决此问题。作为一般规则,我建议不要忽略信号,但在这种情况下,这似乎是合理的。

\n
\n\n

因此修改后的代码现在是:

\n\n
-(void)tee:(NSNotification *)notification {\n    NSData *readData = notification.userInfo[NSFileHandleNotificationDataItem];\n    totalDataRead += readData.length;\n    //    NSLog(@"Total Data Read %ld",totalDataRead);\n    NSArray *pipes = [teeBranches objectForKey:notification.object];\n\n    if (readData.length) {\n        for (NSPipe *pipe in pipes ) {\n            NSInteger numTries = 3;\n            size_t bytesLeft = readData.length;\n            while (bytesLeft > 0 && numTries > 0 ) {\n                ssize_t amountSent= write ([[pipe fileHandleForWriting] fileDescriptor], [readData bytes]+readData.length-bytesLeft, bytesLeft);\n                if (amountSent < 0) {\n                     NSLog(@"write fail; tried %lu bytes; error: %zd", bytesLeft, amountSent);\n                    break;\n                } else {\n                    bytesLeft = bytesLeft- amountSent;\n                    if (bytesLeft > 0) {\n                        NSLog(@"pipe full, retrying; tried %lu bytes; wrote %zd", (unsigned long)[readData length], amountSent);\n                        sleep(1);  //probably too long, but this is quite rare\n                        numTries--;\n                    }\n                }\n            }\n            if (bytesLeft >0) {\n                if (numTries == 0) {\n                    NSLog(@"Write Fail4: couldn\'t write to pipe after three tries; giving up");\n                 }\n                 [self rescheduleOnMain];\n             }\n\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n