Usage of NSException in iPhone Apps

喜欢而已 提交于 2019-11-27 11:44:08

In short:

Do not use exceptions to indicate anything but unrecoverable errors

It is only appropriate to use @try/@catch to deal with unrecoverable errors. It is never appropriate to use @throw/@try/@catch to do control-flow like operations on iOS or Mac OS X. Even then, consider carefully whether you are better off using an exception to indicate an unrecoverable error or simply crashing (call abort()); a crash often leaves behind significantly more evidence.

For example, it would not be appropriate to use for catching out-of-bounds exceptions unless your goal is to catch them and somehow report the error, then -- typically -- crash or, at the least, warn the user that your app is in an inconsistent state and may lose data.

Behavior of any exception thrown through system framework code is undefined.


Can you explain the "Behavior of any exception thrown through system framework code is undefined." in detail?

Sure.

The system frameworks use a design where any exception is considered to be a fatal, non-recoverable, error; a programmer error, for all intents and purposes. There are a very limited number of exceptions (heh) to this rule.

Thus, in their implementation, the system frameworks will not ensure that everything is necessarily properly cleaned up if an exception is tossed that passes through system framework code. Sine the exception is, by definition, unrecoverable, why pay the cost of cleanup?

Consider this call stack:

your-code-1()
    system-code()
        your-code-2()

I.e. code where your code calls into system code which calls into more of your code (a very common pattern, though the call stacks are obviously significantly deeper).

If your-code-2 throws an exception, that the exceptions passes over system-code means the behavior is undefined; system-code may or may not leave your application in an undefined, potentially crashy or data-lossy, state.

Or, more strongly: You can't throw an exception in your-code-2 with the expectation that you can catch and handle it in your-code-1.

I've used exception handling for my fairly intensive audio app with no problems whatsoever. After much reading and a bit of benchmarking and disassembly analysis, I've came to the controversial conclusion that there is no real reason not to use them (intelligently) and plenty of reason not to (NSError pointer-pointers, endless conditionals...yuck!). Most of what people are saying on the forums is just repeating the Apple Docs.

I go into quite a bit of detail in this blog post but I'll outline my findings here:

Myth 1: @try/@catch/@finally is too expensive (in terms of CPU)

On my iPhone 4, throwing and catching 1 million exceptions takes about 8.5 seconds. This equates to about only 8.5 microseconds for each. Expensive in your realtime CoreAudio thread? Maybe a bit (but you'd never throw exceptions there would you??), but an 8.5μs delay in the UIAlert telling the user there was a problem opening their file, is it going to be noticed?

Myth 2: @try Blocks Have a Cost On 32bit iOS

The Apple docs speak of "zero-cost @try blocks on 64bit" and state that 32 bit incurs a cost. A little benchmarking and disassembly analysis seems to indicate that there is zero-cost @try blocks on 32bit iOS (ARM processor) as well. Did Apple mean to say 32bit Intel?

Myth 3: It Matters that Exceptions Thrown Through Cocoa Frameworks are Undefined

Yes, they are "undefined", but what are you doing throwing through the Apple framework anyway? Of course Apple doesn't handle them for you. The whole point of implementing exception handling for recoverable errors is to handle them locally - just not every-single-line "locally".

One edge case here is with methods like NSObject:performSelectorOnMainThread:waitUntilDone:. If the later parameter is YES, this acts like a synchronous function in which case you might be forgiven for expecting the exception to bubble up to your calling scope. For example:

/////////////////////////////////////////////////////////////////////////
#pragma mark - l5CCThread
/////////////////////////////////////////////////////////////////////////

@interface l5CCThread : NSThread @end

@implementation l5CCThread

- (void)main
{
    @try {

        [self performSelectorOnMainThread:@selector(_throwsAnException) withObject:nil waitUntilDone:YES];

    } @catch (NSException *e) {
        NSLog(@"Exception caught!");
    }
}
- (void)_throwsAnException { @throw [NSException exceptionWithName:@"Exception" reason:@"" userInfo:nil]; }

@end

/////////////////////////////////////////////////////////////////////////
#pragma mark - l5CCAppDelegate
/////////////////////////////////////////////////////////////////////////

@implementation l5CCAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    l5CCThread *thd = [[l5CCThread alloc] init];
    [thd start];

    return YES;
}
// ...

In this case the exception would pass "through the cocoa framework" (the main thread's run loop) missing your catch and crashing. You can easily work around this using GCD's dispatch_synch and putting in it's block argument the method call plus any exception handling.

Why Use NSException's over NSError's

Anyone who's ever done work in one of the older C-based frameworks like Core Audio knows what a chore it is checking, handling and reporting errors. The main benefit @try/@catch and NSExceptions provides is to make your code cleaner and easier to maintain.

Say you have 5 lines of code which work on a file. Each might throw one of, say, 3 different errors (eg out of disk space, read error, etc.). Instead of wrapping each line in a conditional which checks for a NO return value and then outsources the NSError pointer investigation to another ObjC method (or worse, uses a #define macro!), you wrap all 5 lines in a single @try and handle each error right there. Think of the lines you'll save!

By creating NSException subclasses, you can also easily centralise error messages, and avoid having your code littered with them. You can also easily distinguish your app's "non-fatal" exceptions from fatal programmer errors (like NSAssert). You can also avoid the need for the "name" constant (the subclass's name, is the "name").

Examples of all this and more details about the benchmarks and disassembly are on this blog post...

Exceptions plus try/catch/finally is the paradigm used by pretty much every other major language (C++, Java, PHP, Ruby, Python). Maybe its time to drop the paranoia and embrace it as well...at least in iOS.

Generally ask yourself if you're trying to signal an error, or do you actually have an exceptional condition? If the former, then it's a very bad idea regardless of any performance issue, real or perceived. If the latter, it's most definitely the right thing to do.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!