Cocoa:异常

来源:互联网 发布:linux查看oracle实例 编辑:程序博客网 时间:2024/06/14 07:15

写在前面

异常(Exception)与错误(Error)有什么区别呢?

在Cocoa中,错误往往是可以解决的,比如网路未连接,路径错误等。这些都可以用一个错误代码表示,处理起来也很简单:重新连接网络,或者重新选择路径即可,程序原有的流程并没有受到影响

而异常,往往是很严重的问题,比如:缺少某个框架,系统版本过低,某些方面不兼容,等等。这些问题并不是程序简简单单就能解决的,这些问题对于程序往往是灾难性的,而且程序会马上崩溃,程序的流程收到了严重的影响。这个叫做异常。

Cocoa中提供了NSException 和 NSError来分别处理这两种情况,但后者不属于该文章的范畴.

如果收到一个异常,应当尽快保存数据,提供消息后退出程序。如果这个异常很容易处理,并且消除,请尝试使用错误来代替。

请避免异常的滥用,异常本身是最后一道防线。而且接下来会讲到,异常完全改变了程序的流程,并且本身就会引起很多问题,尤其是棘手的内存问题,同时异常机制本身效率就很低,特别是在早期的OS X 上,所以,请尽量减少异常的使用,考虑是否能用错误代替


引言

Cocoa 中在程序出现问题的时候往往会抛出一个异常.

比如:发送一个未知消息时,如果没有对其进行进一步处理,会由 NSObject 抛出一个 NSInvalidArguement 的异常,这个异常是由:

-doesNotRecongizeSelector: 抛出的。

NSString* str = [[NSString alloc] init];[str performSelector:@selector(output:)];

异常类

Exceptions 在Cocoa中 被 NSException 所封装,这是Foundation架构的一部分。该类允许开发者创建,抛出一个异常,同时还允许查看异常处的调用栈信息

类主要信息有这些:

@interface NSException : NSObject <NSCopying, NSCoding> {    @private    NSString*name;    NSString*reason;    NSDictionary*userInfo;    idreserved;}+ (NSException *)exceptionWithName:(NSString *)name reason:(NSString *)reason userInfo:(NSDictionary *)userInfo;- (instancetype)initWithName:(NSString *)aName reason:(NSString *)aReason userInfo:(NSDictionary *)aUserInfo;@property (readonly, copy) NSString *name;@property (readonly, copy) NSString *reason;@property (readonly, copy) NSDictionary *userInfo;@property (readonly, copy) NSArray *callStackReturnAddresses;@property (readonly, copy) NSArray *callStackSymbols;- (void)raise;@end@interface NSException (NSExceptionRaisingConveniences)+ (void)raise:(NSString *)name format:(NSString *)format, ... ;+ (void)raise:(NSString *)name format:(NSString *)format arguments:(va_list)arg@end


异常的抛出

1、使用 @throw 指令:

@throw [[NSException alloc] initWithName:@"Exception"  reason:@"I Don‘t Know Why"userInfo:nil];
@throw NSInvalidArgumentException;
2、使用  -raise 方法:
[[NSException exceptionWithName:@"Exception"                          reason:@"I Dont't Know Why"                       userInfo:nil] raise];

这两者的区别在于,前者可以抛出任何异常,只要是ObjC对象就可以,而后者只能是NSException对象

Cocoa框架强类建议开发者只抛出 NSException 或者其子类的异常,不要抛出其他类型的异常


异常的捕获与处理

像很多语言一样,Cocoa在处理异常的时候,使用的是 try...catch结构,但这里增加了finally,如下:

@try {//异常域,其中包含潜在的会抛出异常的部分}@catch (id e) {//用来捕获异常域中抛出的异常,接受的参数推荐为id ,当然也可以是其它类型,比如 NSString。//处理 @try块 中抛出的异常}@finally {//无论异常是否发生,都会执行的代码块}


流程如下:

1、执行异常域,如果没有异常发生,则跨过@catch 执行@finally

2、如果出现异常,异常域以后的代码停止执行,执行@catch,执行@finally。如果此时异常被捕获,则执行@finally以后的部分,如果执行完@finally之后异常依然存在,跳转到上一级。

一旦发生异常,异常域的代码停止执行(无论是否执行完)
@catch只有在发生异常的时候才会执行
@finally无论是否发生异常都会执行

尽管你可以抛出其他类型的异常,但是Cocoa框架本身仅能捕获 NSException 的异常,如果你抛出了其他类型的异常,但并不一定会引发Cocoa的捕获机制。相反,他有可能被其他的东西捕获。因此,我们应该遵循以下的原则:抛出NSException的异常,捕获id的异常。

捕获的异常是只读的

异常抛出后,无论调用层次有多深,都会不断的向上退栈,直到被@catch捕获或者抛出未捕获的异常,也就意味着,深层次中的异常可以在浅层次中捕获

你可以将一组@catch块排列起来,用来捕获多种异常,往往遵循由特殊到一般的原则


@try {//异常域,其中包含潜在的会抛出异常的部分}@catch (NSException *e) {//如果是 NSException 类型的异常,则...}@catch (NSString *e) {//如果是 NSString 类型的异常,则...}@catch (id e) {//其他异常}@finally {//无论异常是否发生,都会执行的代码块</span>}

按照从上到下的顺序匹配,直到找到匹配的@catch或者遇到@finally

异常只能被捕获一次,一旦捕获,就会跨过其他@catch块,直接执行@fianlly

如果你使用了Cocoa异常机制,请尽量避免使用原始的 setjmp() longjmp() 函数来处理异常,因为这种跨作用域的跳转函数如果与@try...@catch 结构交叉会打乱原有的程序流。但是你依然可以使用 goto return exit 这些语句。

异常重抛出

如果捕获的异常无法处理,我们可以把捕获的异常重抛出,交由上一层来处理,方法很简单

在@catch中 向捕获的对象发送 -raise消息,或者是直接 @throw;(不用指明抛出什么,默认抛出以捕获的异常)

@try {[self doSomething];}@catch(NSException e) {@throw;//[e raise];}@finally {[self cleanUp];}


异常嵌套

异常处理可以是嵌套的,而且深层异常可以反馈到浅层,并且当作同层异常处理。

如图,异常在 Method3 中抛出,被 @catch(3)捕获,它并不能处理该异常,发生异常的重抛出,在@finally(3)执行完毕后,程序流返回上一层 Method2,重抛出的异常再次被捕获,程序流转到 @catch(2),重抛出,@finally(2),Method1 @catch(1),重抛出,@finally(1),如果以上再没有捕获动作,将会引发 UncaughtException 

同一层的@finally(如果有) 执行完毕之后,程序流会继续执行@finally以后的代码,然而,如果发生异常重抛出,则是在执行完同层的@finally 之后,程序跳转到上一层。
一定是在本层的@finally执行完毕后,上一层的@catch才会捕捉到重抛出的异常


异常与内存管理

由于异常打断了原有正常的程序流,所以会出现很多问题尤其是内存问题。

手动内存计数环境

- (void)doSomething {    NSMutableArray *anArray = [[NSMutableArray alloc] initWithCapacity:0];    [self doSomethingUnsafe:anArray];    [anArray release];} 

假设 -doSomethingUnsafe: 方法 抛出了异常,而目前的代码块并没有@catch块,按照以上所讲述的,程序流会直接从 -doSomethingUnsafe: 跳转到上一层代码,也就意味着 [anArray release]; 并不会执行,这里就出现了内存泄漏问题,所以正确写法应该是这样:

- (void)doSomething {    NSMutableArray *anArray = nil;    array = [[NSMutableArray alloc] initWithCapacity:0];    @try {        [self doSomethingElse:anArray];    }    @finally {        [anArray release];    } } 

正因为 @finally 无论是否出现异常一定会执行,因此可以把释放语句放进去(包括内存释放,或者是线程中的锁)

Tips:@finally块可以用来 释放内存,资源锁,等等,这样避免一个异常时引发其他问题

异常僵尸

也许很多人会这样想,在@try开始之前我使用 一个 NSAutoreleasePool ,然后在@finally中 把池子释放掉就OK了~

这的确是一个很棒的想法:

- (void)doSomething {    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];    NSMutableArray *anArray = [[[NSMutableArray alloc] initWithCapacity:0]autorelease];    @try {        [self doSomethingElse:anArray];    }    @finally {        [pool release];    }}

但是会出现一个更加隐蔽的问题: 

假设这个方法运行之前已经创建了一个释放池,假设为 POL 

1、方法执行,创建释放池 pool

2、创建可变数组,同时加入释放池 pool

3、抛出异常

4、执行@finally块,销毁释放池!!!

5、返回上一层(假设该异常被上层捕获)

6、对异常操作!!!!!

在第六步湖会出现问题:

这里异常也是自动释放的,所以异常本身加入释放池 pool

然而在执行完步骤四的时候,异常已经在释放池中销毁了,所以返回上一层的异常是一个野指针。

所以正确的写法应该是这样:

- (void)doSomething {    id savedException = nil;    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];    NSMutableArray *anArray = [[[NSMutableArray alloc] initWithCapacity:0]autorelease];    @try {        [self doSomethingElse:anArray];    }    @catch (NSException *theException) {        savedException = [theException retain];@throw; }     @finally {        [pool release];        [savedException autorelease];    }} 

先把异常捕获一次,retain一次,之后重抛出,在自动释放池销毁后,重新加入一次释放池,此时的释放池是上一层的释放池(因为释放池是栈式的)。


ARC环境

即便是ARC,在碰上异常时也会出问题,因为在栈解退的时候,原先保有的对象并不会正常返回或者处理,因此,要在@finally中将指针置为nil以启动垃圾回收

ARC下 NSAutoreleasePool 被禁用,转而使用 @autoreleasepool指令,但是本人才疏学浅,并不太清楚@autoreleasepool 具体是怎么实现的,只知道,指令随即被处理成了一个结构体,链入了一个外部的函数:

<span style="font-size:14px;">/* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool;     {        //insert code here    } }extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);struct __AtAutoreleasePool {  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}  void * atautoreleasepoolobj;};</span><span style="font-size:24px;"></span>

但往下就追踪不下去了,所以希望有高人指点一下,是否会出现泄漏,以及正确的写法


异常未捕获

通常情况下会直接崩溃,但是Cocoa提供了NSSetUncaughtExceptionHandler()

NSGetUncaughtExceptionHandler() 作为最后的防线

Tips:Cocoa中,主线程上的异常并不会引发 未捕获异常处理机制,因为全局的应用程序对象总是捕获所有的异常


预定义的异常

在头文件 NSException.h 中可以找到 Cocoa提供了一些全局的已经预定义的异常:

   NSGenericException   NSRangeException   NSInvalidArgumentException   NSInternalInconsistencyException   NSObjectInaccessibleException   NSObjectNotAvailableException   NSDestinationInvalidException   NSPortTimeoutException   NSInvalidSendPortException   NSInvalidReceivePortException   NSPortSendException   NSPortReceiveException

他们都是NSString类型(这也就是为什么捕获的时候,请使用 id)

除此之外,在Cocoa中还有一些类拥有自己定义的异常,并且均以NS开头。所以在自定义异常的时候注意命名冲突

0 0