iOS开发之断言详解
断言
断言(assertion)是指在开发期间使用的、让程序在运行时进行自检的代码(通常是一个子程序或宏)。断言为真,则表明程序运行正常,而断言为假,则意味着它已经在代码中发现了意料之外的错误。断言对于大型的复杂程序或可靠性要求极高的程序来说尤其有用。
使用原则
有关断言的详细信息,推荐大家一定去看《代码大全2》中“防御式编程”这一章。下面,摘录一些代码大全中有关断言使用的经典指导性建议:
用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。
避免把需要执行的代码放到断言中
用断言来注解并验证前条件和后条件
对于高健壮性的代码,应该先使用断言再处理错误
对来源于内部系统的可靠的数据使用断言,而不要对外部不可靠的数据使用断言,对于外部不可靠数据,应该使用错误处理代码。断言可以看成可执行的注释。
系统外部的数据(用户输入,文件,网络读取等等)都是不可信的,需要严格检查(通常是错误处理)才能放行到系统内部,这相当于一个守卫。而对于系统内部的交互(比如子程序调用),如果每次也都去处理输入的数据,也就相当于系统没有可信的边界了,会让代码变的臃肿复杂;而事实上,在系统内部,传递给子程序预期的恰当数据应该是调用者的责任,系统内的调用者应该确保传递给子程序的数据是恰当可以正常工作的。这样一来,就隔离了不可靠的外部环境和可靠的系统内部环境,降低复杂度。
但是在开发阶段,代码极可能包含缺陷,也许是处理外部数据的程序考虑的不够周全,也许是调用系统内部子程序的代码存在错误,造成子程序调用失败。这个时候,断言就可以发挥作用,用来确诊到底是那部分出现了问题而导致子程序调用失败。在清理了所有缺陷之后,内外有别的信用体系就建立起来。等到发行版时候,这些断言就应该没有存在必要。
不同版本之间的断言使用
在iOS开发中,可以使用宏NSAssert()在程序中进行断言处理。NSAssert()使用正确,可以帮助开发者尽快定位bug。开发者没有必要在应用程序的每个版本中都进行断言检查,这是因为大多数项目都是有两个版本:Debug版和Release版。在Debug版中,开发者希望所有的断言都检查到,而在Release版中,往往都是禁用断言检查的。设置Release版本中禁用断言的方法如下:
在Build Settings菜单,找到Preprocessor Macros项,Preprocessor Macros项下面有一个选择,用于程序生成配置:Debug版和Release版。选择 Release项,设置NS_BLOCK_ASSERTIONS,不进行断言检查。如下图所示
下面,我们在一个 打印名字的函数里面,加入断言,以使程序在发现输入的名字为空时,抛出异常。1
2
3
4
5- (void)printMyName:(NSString *)myName
{
NSAssert(myName != nil, @"名字不能为空!");
NSLog(@"My name is %@.",myName);
}
当传给函数的参数(myName)为空时,断言将被执行,程序Crash,并打印出断言中的描述信息。本例中,在控制台打印出了如下的日志:
1 | NSAssert[1268:a0b] *** Assertion failure in -[ViewController printMyName:] |
断言告诉我们,传入的参数不能为空,通过这个报错很容易就能确定错误发生的原因及位置。
如果,我们传入非空的参数,则程序会正确打印出传入的名字:My name is UnivCore.
下面,我们将测试程序设置为Release版本,依据之前的设定,即使当传入的参数为空时,断言也不会被执行。设置为Release版本的方法如下:
依次点击Product->Scheme->Edit Scheme…(也可以直接按快捷键command + shift + ,),选择Run 项,然后在Info面板上修改Build Configuration为Release,就可以将当前的生成配置改为Release。然后,生成并运行程序,就会生成Release版本的程序。注意,对于Archive 项,默认的生成配置就是Release。
此时,我们再运行程序,程序会打印如下语句:
My name is (null).
这说明,断言代码没有运行。
苹果优化:
从 Xcode 4.2 开始,发布构建默认关闭了断言,它是通过定义 NS_BLOCK_ASSERTIONS 宏实现的。也就是说,当编译发布版时,任何调用 NSAssert 等的地方都被有效的移除了。不用我们再手动处理了。
避免把要执行的代码放到断言中
根据以上讲述,断言的可以在编译器中设置关闭,如果你把一些操作写在断言里,在某些情况下可能编译器会过滤掉这些代码。所有我们应该避免把要执行的代码放到断言中。
使用上的注意点
1.仔细观察 NSAssert 的宏定义 ,你会发现 self 的痕迹,有 self 的地方就一定要注意 block 容易产生的循环引用问题。
1 | /** NSAssert */ |
例如:我在自己定义的block中使用了NSAssert导致循环引用1
2
3
4
5mc = @"mingzhi";
self.block = ^(int num){
NSAssert(mc == nil, @"我不为空了");
NSLog(@"%d",num);
};
我们会看到Block中循环引用的警告:
那如果我想在 Block 中使用断言怎么办呐?用 NSCAssert 替换 NSAssert,NSCParameterAssert 来替换 NSParameterAssert,就不会出现循环引用的问题了。
mc = @"mingzhi";
self.block = ^(int num){
NSCAssert(mc == nil, @"我不为空了");
NSLog(@"%d",num);
};
这样就解决了循环引用的问题了。
2.使用NSAssert的时候,每个NSAssert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观地判断是那个条件失败。
3.频繁地调用会极大的影响程序的性能,增加额外开销。
什么时候使用断言
我们在什么时候应该使用断言呢?通常我们期望程序按照我们的预期去运行时,如调用的参数为空时流程就无法继续下去时,可以使用断言。但另一方面,我们也需要考虑,在这加断言确实是需要的么?我们是否可以通过更多的容错处理来使程序正常运行呢?
号外
推荐大家扫码关注下面二维码,iOS和Mac干货不断: