Cocoa:异常

写在前面

异常(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;
    id			reserved;
}

+ (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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值