licc

心有猛虎 细嗅蔷薇

0%

NSAssert详解及C语言assert的一些坑


assert大家都不陌生,在开发阶段用断言函数可以很好的进行调试。

一般OC中,我们使用的是NSAssert。在Xcode4.2以后,Xcode会在release环境下自动把NSAssert忽略掉,确保release环境不会出错。

assert的坑

最近遇到一个问题,线上Crash定位在了一个pod库中的assert方法,在OC里面调用assert是用的C语言的assert。结果在release环境下也生效了,直接造成了Crash。

查找了一些资料,找到了解决方案。

通过查询源码发现,在C语言中是如何忽略的assert

#undef assert
#undef __assert

#ifdef NDEBUG
#define assert(e) ((void)0)
#else

#ifndef __GNUC__

源码地址详见:https://opensource.apple.com/source/Libc/Libc-583/include/assert.h.auto.html

NDEBUG是C语言中的一个标准宏,其语义适用于C89,C99,C ++ 98,C ++ 2003,C ++ 2011,C ++ 2014标准。
详见:https://stackoverflow.com/questions/2290509/debug-vs-ndebug

根据源码可知,如果定义了NDEBUG,那么assert就返回void,就不会再生效。

那么我们只需要在主工程中,选择Bulid Settings选择Preprocessor MarcosRelease中添加NDEBUG=1即可。

但是需要注意的是,只修改主工程并不会对pod库生效,所以还需要对pod库进行处理!

在pod库中一个一个修改太麻烦,并且每次pod install后就会失效,我们需要从podfile文件入手。

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0'
if config.name == 'Release'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'NDEBUG=1']
end
end
end
end

判断是否是Release环境,如果是就在build_settings中的GCC_PREPROCESSOR_DEFINITIONS添加$(inherited)NDEBUG=1

$(inherited)是继承Pods工程的设置

添加完后执行pod install,就可以发现每个pod库的Preprocessor Marcos中,在Release下都添加了NDEBUG=1

NSAssert介绍

说完assert,再来说说我们常用的NSAssert

NSAssert细分为

  • NSAssert/NSCAsseet
  • NSParameterAssert / NSCParameterAssert

他们的区别如下:

  1. NSAssert 和 NSParameterAssert 只适用于OC环境中。NSCAsseet 和 NSCParameterAssert 适用于C语言环境中

  2. 当 NSAssert 或 NSParameterAssert 的条件不满足时,断言处理器会调用
    -handleFailureInMethod:object:file:lineNumber:description:方法。
    当 NSCAssert 或 NSCParameterAssert 的条件不满足时,断言处理器会调用 -handleFailureInFunction:file:lineNumber:description: 方法。

NSAssert和NSAssert都有一些变体,例如NSAssert1NSAssert2NSCAssert1NSCAssert2等等。他们的区别是会输出不同的参数。
具体信息详见官方文档:https://developer.apple.com/documentation/foundation/nsassert1

从Xcode4.2以后,会自动在release环境下忽略NSAssert等断言函数。它是通过定义 NS_BLOCK_ASSERTIONS 宏实现的,确保不会对release环境有影响。

我们可以在很多第三方库中看到他们定义的assert,关闭条件也是基于NS_BLOCK_ASSERTIONS

#ifndef _GTMDevAssert
// we directly invoke the NSAssert handler so we can pass on the varargs
// (NSAssert doesn't have a macro we can use that takes varargs)
#if !defined(NS_BLOCK_ASSERTIONS)
#define _GTMDevAssert(condition, ...) \
do { \
if (!(condition)) { \
[[NSAssertionHandler currentHandler] \
handleFailureInFunction:(NSString *) \
[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \
file:(NSString *)[NSString stringWithUTF8String:__FILE__] \
lineNumber:__LINE__ \
description:__VA_ARGS__]; \
} \
} while(0)
#else // !defined(NS_BLOCK_ASSERTIONS)
#define _GTMDevAssert(condition, ...) do { } while (0)
#endif // !defined(NS_BLOCK_ASSERTIONS)

NSAssertNSCAssert底层都是通过NSAssertionHandler来实现的,只有2个实现方法

  • -handleFailureInMethod:object:file:lineNumber:description: NSAssert/NAParamterAssert调用
  • -handleFailureInFunction:file:lineNumber:description: NSCAssert/NACParamterAssert调用

Xcode的开关在

我们也可以自定义NSAssert

继承NSAssertionHandler,重写handleFailureInFunction:file:lineNumber:descriptionhandleFailureInMethod:object:file:lineNumber:description:

在AppDelegate中注册

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSAssertionHandler *handler = [[UDTestAssertHandler alloc] init];
[[[NSThread currentThread] threadDictionary] setValue:handler
forKey:NSAssertionHandlerKey];
return YES;
}

NSAssert的坑

NSAssert在OC中,会存在循环引用问题

- (IBAction)buttonAction:(UIButton *)sender {
TestMode *mode = [TestMode new];
self.mode = mode;
// @weakify(self);
mode.textBlock = ^{
// @strongify(self);
int k = 0;
NSParameterAssert(k == 0);
NSLog(@"123");

};
mode.textBlock();
}

循环引用的根源在于NSAssert定义中使用了self

#if !defined(_NSAssertBody)
#define NSAssert(condition, desc, ...) \
do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (__builtin_expect(!(condition), 0)) { \
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
object:self file:@(__FILE_NAME__) \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
} \
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#endif

我们可以试用@weakself 和 @strongself来解决。不过在release环境下,assert并不会调用。所以推荐试用NSCAssertNSParameterAssert