问题起源
一个 Code Review ,具体实现功能不用看。请注意231行,延迟1s执行的方法是delayPerformSelector:,参数是NSNumber *类型。这里面把它转成BOOL值以后又调用了setRecordButtonEnable:方法。为什么不能直接调用而要加一层呢?因为无论给什么参数,得到的enable总是NO。
【我觉得这里还可以再去了解一下,performSelector:和直接调用函数有什么区别?有什么应用场景?】

经过简单的搜索可以确定,因为performSelector:withObject:方法的 object 参数是id,因此直接转 BOOL 是会出问题的。甚至在《Effective ObjC:52建议》的第42条就指出,多用GCD,少用performSelector。
但究竟为什么总是NO呢?
一探究竟
在 XCode 中随便建立一个 iOS 工程,写如下代码
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(handler:) withObject:@YES];
}
- (void)handler:(id)arg {//arg (__NSCFBoolean *)0x1068a05e0
if([arg boolValue]) {
NSLog(@"Haha!");
}
}
//print Haha!
- (void)handler:(BOOL)arg {
if(arg) { //always NO
NSLog(@"Haha!");
}
}
//print nothing
首先可以确定的是,直接传BOOL参数是错误的写法。因为传参是对象,因此必须要接收 id 类型的 arg 并 unbox。
这里出现了一个陌生的类__NSCFBoolean是什么?
看文章BOOL / bool / Boolean / NSCFBoolean,比较详细地介绍了 Objective C 中的几种 BOOL 。一个重要的启发:
NSCFBoolean is a private class in the NSNumber class cluster. It is a bridge to the CFBooleanRef type, which is used to wrap boolean values for Core Foundation property lists and collections. CFBoolean defines the constants kCFBooleanTrue and kCFBooleanFalse. Because CFNumberRef and CFBooleanRef are different types in Core Foundation, it makes sense that they are represented by different bridging classes in NSNumber.
NSCFBoolean是NSNumber类簇中的一个私有的类。它是通往CFBooleanRef类型的桥梁,它被用来给Core Foundation的属性列表和集合封装布尔数值。CFBoolean定义了常量kCFBooleanTrue和kCFBooleanFalse。因为CFNumberRef和CFBooleanRef在Core Foundation中是不同类型,所以在NSNumber被以不同的衔接类表示是有道理的。 在《Cocoa and Objective C: Up and Running》中有如下内容:


继续深入……
感觉解释还不够清楚?那只能看源码了。
//CFRuntime.h
typedef struct __CFRuntimeBase {
uintptr_t _cfisa;
uint8_t _cfinfo[4];
#if __LP64__
uint32_t _rc;
#endif
} CFRuntimeBase;
#if __BIG_ENDIAN__
#define INIT_CFRUNTIME_BASE(...) {0, {0, 0, 0, 0x80}}
#else
#define INIT_CFRUNTIME_BASE(...) {0, {0x80, 0, 0, 0}}
#endif
//外层的花括号是结构体,内层的花括号是4个元素的数组。
//CFNumber.h
typedef const struct __CFBoolean * CFBooleanRef;
CF_EXPORT
const CFBooleanRef kCFBooleanTrue;
CF_EXPORT
const CFBooleanRef kCFBooleanFalse;
//CFNumber.c
struct __CFBoolean {
CFRuntimeBase _base;
};
static struct __CFBoolean __kCFBooleanTrue = {
INIT_CFRUNTIME_BASE()
};
const CFBooleanRef kCFBooleanTrue = &__kCFBooleanTrue;
static struct __CFBoolean __kCFBooleanFalse = {
INIT_CFRUNTIME_BASE()
};
const CFBooleanRef kCFBooleanFalse = &__kCFBooleanFalse;
可以看出,其实 __kCFBooleanTrue 和 __kCFBooleanFalse 是完全相同的结构体,他们对应的kCFBooleanTrue和kCFBooleanFalse只是地址不同(至于为什么当初有这样奇怪的设计还没查到)。这里不深入研究了(也没法深入研究,Google可考资料特别少,可能太偏门了),因为这个Ref是用于封装布尔值的,而本文开头的问题似乎与解包的过程相关。
本源:BOOL 的定义
由于 ObjC 与 C 的紧密联系,迫使我们必须回到 C 本身来寻找 BOOL 的定义。
//gcc/ginclude/stdbool.h
#ifndef __STDBOOL_H
#define __STDBOOL_H
/* Don't define bool, true, and false in C++, except as a GNU extension. */
#ifndef __cplusplus
#define bool _Bool
#define true 1
#define false 0
#else /* __cplusplus */
/* Supporting _Bool in C++ is a GCC extension. */
#define _Bool bool
#if __cplusplus < 201103L
/* For C++98, define bool, false, true as a GNU extension. */
#define bool bool
#define false false
#define true true
#endif
#endif
/* Signal that all the definitions are present. */
#define __bool_true_false_are_defined 1
#endif /* __STDBOOL_H */
这破代码真是超烦的!几个bool宏定义来定义去要怎样!当初为了定义一个bool也是操碎了❤️……
Boolean type
_Bool (also accessible as the macro bool) - type, capable of holding one of the two values: 1 and 0 (also accessible as the macros true and false). Note that conversion to _Bool does not work the same as conversion to other integer types: (bool)0.5 evaluates to 1, whereas (int)0.5 evaluates to 0.
_BOOL 是 C99 的关键字。至于为什么写法这么奇怪,有一定的历史原因。可以参考Quora的问题In C programming, what is the difference between bool and _Bool?。而bool真的是 C++ 的关键字。
__cplusplus
This macro is defined when the C++ compiler is in use. You can use
__cplusplusto test whether a header is compiled by a C compiler or a C++ compiler. This macro is similar to__STDC_VERSION__, in that it expands to a version number. Depending on the language standard selected, the value of the macro is 199711L for the 1998 C++ standard, 201103L for the 2011 C++ standard, 201402L for the 2014 C++ standard, 201703L for the 2017 C++ standard, or an unspecified value strictly larger than 201703L for the experimental languages enabled by -std=c++2a and -std=gnu++2a.
因此上面一段翻译成人话就是:
if 用的 C 编译器:
宏定义 bool 就是 _Bool
宏定义 true 就是 1
宏定义 false 就是 0
else: // 用的 C++ 编译器
宏定义 _BOOL 就是 bool(用C++的关键字兼容C)
if 是 C++98 标准:
宏定义 bool 就是 bool
宏定义 true 就是 true
宏定义 false 就是 false
有三句像是废话一样的宏定义是在干什么的呢?参考StackOverflow提问 。(其实没看懂……
回到 Objective C
查看objc.h定义,发现即使 BOOL 也不是 signed char 的 typedef 这么简单。
//objc.h
/// Type to represent a boolean value.
#if defined(__OBJC_BOOL_IS_BOOL)
// Honor __OBJC_BOOL_IS_BOOL when available.
# if __OBJC_BOOL_IS_BOOL
# define OBJC_BOOL_IS_BOOL 1
# else
# define OBJC_BOOL_IS_BOOL 0
# endif
#else
// __OBJC_BOOL_IS_BOOL not set.
# if TARGET_OS_OSX || (TARGET_OS_IOS && !__LP64__ && !__ARM_ARCH_7K)
# define OBJC_BOOL_IS_BOOL 0
# else
# define OBJC_BOOL_IS_BOOL 1
# endif
#endif
#if OBJC_BOOL_IS_BOOL
typedef bool BOOL;
#else
# define OBJC_BOOL_IS_CHAR 1
typedef signed char BOOL;
// BOOL is explicitly signed so @encode(BOOL) == "c" rather than "C"
// even if -funsigned-char is used.
#endif
#define OBJC_BOOL_DEFINED
#if __has_feature(objc_bool)
#define YES __objc_yes
#define NO __objc_no
#else
#define YES ((BOOL)1)
#define NO ((BOOL)0)
#endif
这里面有大量的宏定义。文章iOS-深挖BOOL,对这里面的部分宏定义都进行了研究。
由于想看一下 runtime 源码,因此用源码编译了 libobjc.A.dylib,是为插曲。参考:runtime源码调试。但是因为这个插曲,结合上面的 objc.h 发现了一个问题:macOS 上和 iOS 上的 BOOL 实际上并不一样。实验验证:
//a macOS project
@interface ViewController ()
@property (strong) NSView *greenView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.greenView = [[NSView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.greenView.wantsLayer = YES;
self.greenView.layer.backgroundColor = [NSColor greenColor].CGColor;
[self.view addSubview:self.greenView];
[self performSelector:@selector(handler:) withObject:@YES afterDelay:1];
}
- (void)handler:(BOOL)arg { // (BOOL)232 (=0xe8)
NSLog(@"__OBJC_BOOL_IS_BOOL = %d , OBJC_BOOL_IS_BOOL = %d", __OBJC_BOOL_IS_BOOL, OBJC_BOOL_IS_BOOL);//__OBJC_BOOL_IS_BOOL = 0 , OBJC_BOOL_IS_BOOL = 0
self.view.hidden = arg;
NSLog(@"%d, %d", arg, self.view.hidden); //-24, 0
}
- (void)handler:(id)arg { // (__NSCFBoolean *)0x00007fffa38533e8
self.view.hidden = [arg boolValue];
NSLog(@"%lu, %d", arg, self.view.hidden); //140735936803816, 0
}
@end

对于macOS,参数如果是BOOL,相当于直接取了id值(应该是个@YES对象的地址)的低8位,确实是一个 signed char 类型的数,只是,对于 BOOL 类型的对象的属性,唯有准确赋值1才被认为是 YES,其他情况都是NO。运行来看,绿色View不会在 1s 后隐藏。
而我们通常讲的“只有0是NO,非0值都是YES”,是适用于if等条件语句的规则,这个差异应该注意。
@interface ViewController ()
@property (nonatomic, strong) UIView *greenView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.greenView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.greenView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.greenView];
[self performSelector:@selector(handler:) withObject:@YES afterDelay:1];
}
- (void)handler:(BOOL)arg { // (BOOL)NO
self.greenView.hidden = arg;
NSLog(@"%d, %d", arg, self.greenView.hidden); //0, 0
}
- (void)handler:(id)arg { // (__NSCFBoolean *)0x00000001036a55e0
NSLog(@"__OBJC_BOOL_IS_BOOL = %d, OBJC_BOOL_IS_BOOL = %d",__OBJC_BOOL_IS_BOOL, OBJC_BOOL_IS_BOOL); // __OBJC_BOOL_IS_BOOL = 1, OBJC_BOOL_IS_BOOL = 1
self.greenView.hidden = [arg boolValue]; //隐藏!
NSLog(@"%lu, %d", arg, self.greenView.hidden); //4352267744, 1
}
@end
而 iOS 的BOOL实际上是 C99 标准 stdbool.h 里的 bool(_Bool)。注意这里将任何非零数值赋给hidden时候被转成了YES(虽然一般不会这样做),前文_Bool 的说明里也提到。 对于BOOL类型的arg,我不知道底层做了什么处理(所有资料都是用libobjc.A.dylib调试 macOS 程序),总之它传给handler:时候就已经是NO了。根据前面 macOS 工程的经验,应该也是对同一个地址值对不同解读而已。但是显然这里地址值最低1字节是e0,好像并不应该转成NO。

结论
文章一开头的问题,在找了这么一大圈后,我仍然只知道不该这么写,但是这么写为什么会得到 NO,还是一个谜。我已经把问题放到了Stackoverflow上,希望可以有人来进一步揭开答案。
18/05/07 更新 终于等到了一个靠谱的优秀答案,他长篇大论之后告诉我这就是一个“未定义行为”……