什么是NSAssert?
NSAssert ,断言,其实是一个宏,主要用于开发阶段调试程序,在真机中将会自动忽略。通过为NSAssert()传递条件表达式来断定是否属于Bug,满足条件返回真,程序继续运行。如果返回假,则抛出异常,并且可以自定义异常描述。
断言其实是“防御式编程”的常用的手段。
防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。这种思 想是将可能出现的错误造成的影响控制在有限的范围内。断言能够有效的保证数据的正确性,防止因为脏数据让整个程序运行在不稳定的状态下面。
《代码大全2》中“防御式编程”这一章,是这样概述断言的:
1用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。
2避免把需要执行的代码放到断言中
3用断言来注解并验证前条件和后条件
4对于高健壮性的代码,应该先使用断言再处理错误
5对来源于内部系统的可靠的数据使用断言,而不要对外部不可靠的数据使用断言,对于外部不可靠数据,应该使用错误处理代码。
对来源于内部系统的可靠的数据使用断言,而不要对外部不可靠的数据使用断言,对于外部不可靠数据,应该使用错误处理代码。断言可以看成可执行的注释。
系统外部的数据(用户输入,文件,网络读取等等)都是不可信的,需要严格检查(通常是错误处理)才能放行到系统内部,这相当于一个守卫。而对于系统内部的交互(比如子程序调用),如果每次也都去处理输入的数据,也就相当于系统没有可信的边界了,会让代码变的臃肿复杂;而事实上,在系统内部,传递给子程序预期的恰当数据应该是调用者的责任,系统内的调用者应该确保传递给子程序的数据是恰当可以正常工作的。这样一来,就隔离了不可靠的外部环境和可靠的系统内部环境,降低复杂度。
但是在开发阶段,代码极可能包含缺陷,也许是处理外部数据的程序考虑的不够周全,也许是调用系统内部子程序的代码存在错误,造成子程序调用失败。这个时候,断言就可以发挥作用,用来确诊到底是那部分出现了问题而导致子程序调用失败。在清理了所有缺陷之后,内外有别的信用体系就建立起来。等到发行版时候,这些断言就应该没有存在必要。
每一个线程都有它自己的断言捕获器(一个NSAssertionHanlder的实例),当断言发生时,捕获器会打印断言信息和当前的类名、方法名等信息。然后抛出一个NSInternalInconsistencyException异常让整个程序Crash掉。并且在当前线程的断言捕获器中执行handleFailureInMethod:object:file:lineNumber:description:以上述信息为输出。
宏是怎样定义的?
#define NSAssert(condition, desc, ...)\
do {\
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) {\
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
object:self file:[NSString stringWithUTF8String:__FILE__] \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
}\
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#if !defined(_NSAssertBody)
#define NSAssert1(condition, desc, arg1) NSAssert((condition), (desc), (arg1))
#define NSAssert2(condition, desc, arg1, arg2) NSAssert((condition), (desc), (arg1), (arg2))
#define NSAssert3(condition, desc, arg1, arg2, arg3) NSAssert((condition), (desc), (arg1), (arg2), (arg3))
#define NSAssert4(condition, desc, arg1, arg2, arg3, arg4) NSAssert((condition), (desc), (arg1), (arg2), (arg3), (arg4))
#define NSAssert5(condition, desc, arg1, arg2, arg3, arg4, arg5) NSAssert((condition), (desc), (arg1), (arg2), (arg3), (arg4), (arg5))
#define NSParameterAssert(condition) NSAssert((condition), @"Invalid parameter not satisfying: %s", #condition)
#endif
condition是条件表达式,值为YES或NO,desc为异常描述,通常为NSString。当condition为YES石化程序继续运行,为NO则抛出desc描述的异常信息。NSAssert可以出现在程序的任何一个位置。
NSParameterAssert:
程序在相应位置设定的条件不满足的时候抛出来,用NSParameterAssert让程序crash的时候可以打印出程序crash的位置:
*** Assertion failure in -[ViewController test2], /Users/zhaoxiangguang/Desktop/NSAssertDemo/NSAssertDemo/ViewController.m:55
*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Invalid parameter not satisfying: i == 10’
*** First throw call stack: … …
断言何时运行?
通常在debug情况下(真机中会自动忽略),所有NSAssert都会被执行。在release下系统会自动将断言设置为禁用。如果release下断言仍可执行的话,则需要我们手动设置为,设置方式如下:Preprocessor Macros 的Release值为 NS_BLOCK_ASSERTIONS。
小延伸:如何切换Debug和Release呢?
自答吧:
1、点击工程名,点这里。选择Edit Scheme
2、选择Run,在Info 中将Build Configuration 修改为release
另外C语言调试使用NSCAssert。
使用示例:
NSInteger i = 11;
NSAssert((i == 10), @“i不等于10");
注意:
1、使用NSAssert的时候,每个NSAssert只检验一个条件,因为同事检验多个条件时,如果断言失败,无法直观地判断是那个条件失败。
2、Block中使用NSAssert要特别注意循环引用。为什么会导致循环引用?我们再来查看NSAssert的宏定义:
#define NSAssert(condition, desc, ...)\
do {\
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) {\
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
object:self file:[NSString stringWithUTF8String:__FILE__] \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
}\
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
大家发现没?宏定义展开后会出现对self的持有,容易导致循环引用。
怎么办?这个时候我们可以使用NSCAssert来。
查看NSCAssert的宏定义:
#define NSCAssert(condition, desc, ...) \
do {\
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) {\
[[NSAssertionHandler currentHandler] handleFailureInFunction:[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \
file:[NSString stringWithUTF8String:__FILE__] \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
}\
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#endif
宏定义中并没有出现self,所以在block中使
3、频繁地调用会极大的影响程序的性能,增加额外开销。