×

iOS-深挖BOOL

96
从来吃不胖
2017.01.06 17:03* 字数 1845

为啥要深挖这玩意

你每天都在用BOOL吧?那我就来问一道题:请问BOOL是非0即真吗?

如果不是百分百确定的,请往下看。

BOOL的定义(Xcode7.3版本,位于usr/include/objc/objc中)

/// 位于<objc/objc>头文件中

/// Type to represent a boolean value.
#if (TARGET_OS_IPHONE && __LP64__)  ||  TARGET_OS_WATCH
#define OBJC_BOOL_IS_BOOL 1
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

这是BOOL在SDK中的定义。

1. 让我们展开宏定义来看看:

TARGET_OS_IPHONETARGET_OS_WATCH的定义是在:

TARGET_OS_IPHONE定义

请注意一点,iOS 9.3只是其中的一个SDK,代表的是真机iOS 9.3版本的SDK,在Xcode中有很多的SDK,如:

SDK种类

查看不同的SDK,你会发现TARGET_OS_IPHONETARGET_OS_WATCH所定义的值是不同的。如iOS 9.3中:

#define TARGET_OS_MAC               1
#define TARGET_OS_WIN32             0
#define TARGET_OS_UNIX              0
#define TARGET_OS_IPHONE            1 
#define TARGET_OS_IOS               1
#define TARGET_OS_WATCH             0
#define TARGET_OS_TV                0
#define TARGET_OS_SIMULATOR         0
#define TARGET_OS_EMBEDDED          1 

而在OS X 10.11中:

#define TARGET_OS_MAC               1
#define TARGET_OS_WIN32             0
#define TARGET_OS_UNIX              0
#define TARGET_OS_IPHONE            0 
#define TARGET_OS_IOS               0
#define TARGET_OS_WATCH             0
#define TARGET_OS_TV                0
#define TARGET_OS_SIMULATOR         0
#define TARGET_OS_EMBEDDED          0 

这两个宏定义代表的含义同时也说明了在不同的平台/环境下,部分判断并不是靠“机器”自动进行的,而是靠人为判断并定义的,所以程序并没有那么神奇,它并不能够自动区分iPhone还是iWatch。

__LP64__则是由预处理器定义的宏,代表当前操作系统是64位。该宏在头文件中无法找到的,但是我们还是有方法可以查看的:

a. 可以在终端中输入cpp -dM /dev/null进行查看(当然常规手段只能在Mac上运行该指令):

__LP64__定义1

b. 在代码中对__LP64__进行 if 判断,如:

#if __LP64__
    #define kNum 64
#else
    #define kNum 32
#endif
    NSLog(@"kNum: %d",kNum);

分别选择iPhone5/5c/4s/4等真机和iPhone6/6s等进行运行查看结果(iPhone模拟器也能得出正确值,但非“真实值”)。

c. 通过Xcode助理器的预编译功能查看__LP64__的预编译结果:

__LP64__定义3

2. 伪代码解释:

if (处于64位iPhone 或者 iWatch) {
  BOOL就是bool (非0即真)
} else {
  BOOL就是signed char (1个字节。-128 ~ 127)
}

这里的else,最常见的就是32位CPU的iPhone,如iPhone5c/5/4s/4等。
定义里可以看出,BOOL其实是个宏,那么知道了BOOL背后的数据类型,出了问题就有能力去分析问题。

3. 这里就引申出一个可能存在的问题:

- (void)doSomeThingWithA:(NSInteger)a {
    NSInteger b = 200;
    if (a + b) {
        // 业务代码
    }
}

乍看之下,这段代码只要 a != -200,都是能够执行业务代码部分。在64位iPhone和iWatch机器里确实是如此,因为定义的BOOL就是bool,就是非0即真,你用负数也是真。
但是在32位机器里呢?如果a传入56,a + b == 256,二进制是 0000 0000 0000 0000 0000 0001 0000 0000(因为32位里NSInteger是int,占用4个字节,每个字节8位)。该数值在if时会强转为BOOL(也就是sign char)进行判定:取低8位 0000 0000 来判断,而0000 0000就是0。if中判定为0,则为假,不再执行业务代码。

YES / NO的定义

说到了BOOL,就要说说YES / NO。

#if __has_feature(objc_bool)
#define YES __objc_yes
#define NO  __objc_no
#else
#define YES ((BOOL)1)
#define NO  ((BOOL)0)
#endif

这两玩意也是宏。这里定义了YES / NO在不同条件下的真实类型。

1. 让我们展开宏定义来看看:

__has_feature这个宏是clang提供的一个宏,用来指定在当前语言下是否支持某个“特征”。上述的objc_bool这个特征暂时还没有找到具体的定义,因此让我们来用代码检测下__has_feature(objc_bool)这个条件是否成立。

#if __has_feature(objc_bool)
    #define kHaveFeature 10
#else
    #define kHaveFeature 20
#endif
    
    NSLog(@"kHaveFeature : %d", kHaveFeature);
    
    NSLog(@"__objc_yes : %d",__objc_yes);
    NSLog(@"__objc_no : %d",__objc_no);
    
    NSLog(@"YES : %d",YES);
    NSLog(@"NO : %d",NO);

让我们用Xcode助理器的预编译功能查看这段代码的预编译结果:

YES/NO的定义

经验证,32位与64位真机的预编译结果均如图所示。也就是说当前的iOS系统中,YES与NO的实际类型为__objc_yes__objc_no

2. __objc_yes__objc_no到底又是什么?

但是问题又来了,翻遍usr/include目录,还是找不到__objc_yes的定义,那么它们又是个什么鬼?让我们借助编译器来看看它的类型:

  • 32位下的__objc_yes__objc_no
32位下的__objc_yes
  • 64位下的__objc_yes__objc_no
64位下的__objc_yes

尽管我们无法从SDK的头文件里获取__objc_yes__objc_no的真实类型,但是我们的编译器还是帮助我们找到了它们的真实类型:BOOL。

再让我们实际打印出这两者的值来看看:

NSLog(@"__objc_yes : %d",__objc_yes);
NSLog(@"__objc_no : %d",__objc_no);
    
NSLog(@"YES : %d",YES);
NSLog(@"NO : %d",NO);

// 结果:32位与64位均为:
2016-12-29 20:15:20.321 NewTestBOOL[3574:641831] __objc_yes : 1
2016-12-29 20:15:20.322 NewTestBOOL[3574:641831] __objc_no : 0
2016-12-29 20:15:20.324 NewTestBOOL[3574:641831] YES : 1
2016-12-29 20:15:20.325 NewTestBOOL[3574:641831] NO : 0

3. 这里又引申出一个可能存在的问题:

请问下列代码会打印哪些结果?

BOOL b2 = 2;
NSLog(@"b2 的值 : %d",b2);
if (b2) {
    NSLog(@"b2 为真");
} else {
    NSLog(@"b2 为假");
}
if (b2 == YES) {
    NSLog(@"b2 等于 YES ");
} else {
    NSLog(@"b2 不等于 YES ");
}
  1. b2的值 : 2
  2. b2 为真
  3. b2 不等于 YES

这个答应在32位与64位下应该没问题了~那让我们实际跑起来看看:

  • 32位下:

    2016-12-29 20:44:08.515 NewTestBOOL[3600:645491] b2 的值 : 2
    2016-12-29 20:44:08.516 NewTestBOOL[3600:645491] b2 为真
    2016-12-29 20:44:08.517 NewTestBOOL[3600:645491] b2 不等于 YES 
    

    没毛病~

  • 64位下:

2016-12-29 20:45:03.546554+0800 NewTestBOOL[3337:1547022] b2 的值 : 1
2016-12-29 20:45:03.546564+0800 NewTestBOOL[3337:1547022] b2 为真
2016-12-29 20:45:03.546594+0800 NewTestBOOL[3337:1547022] b2 等于 YES 

有毛病!

让我们仔细分析下64位为何b2会变成1:

64位下b2的类型是BOOL,真实类型为bool,任何不为0的数强转为bool类型,均为转为true,在C99的官方定义中,true为1,所以这里b2变为了1。当然这里还有个疑问:为何不为0的数转为bool均为true?不要告诉我“就是这样的”?如果有人知道可以联系我~(475325435@qq.com

扩展内容

我们来看看C99中的bool定义:

#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
#elif defined(__GNUC__) && !defined(__STRICT_ANSI__)
/* Define _Bool, bool, false, true as a GNU extension. */
#define _Bool bool
#define bool  bool
#define false false
#define true  true
#endif

#define __bool_true_false_are_defined 1

#endif /* __STDBOOL_H */

这是stdbool.h中的bool的定义(C99标准)。
bool其实还是个宏,其真实类型为_Bool_Bool在C99中是作为C语言原生的布尔类型的,也就是和int/float一样是在编译器中预定义的,1为真,0为假,具体占用内存大小还是由编译器决定。定义中的true与false分别为1和0。

总结

宏真的是个好东西~


更新:2017年4月16日

当前最新的iOS版本为10.3.1,Xcode最新版本为8.3.1。

很遗憾Steve Jobs的经典iPhone4在默认的Xcode8中无法真机调试(连线调试),但是还是可以打包进行安装运行的,只不过可能查看日志略麻烦~需要支持iOS7的同学们可能要多费些力。

当前10.3 SDK中已经对BOOL的定义作了少许改动,让我们来瞧瞧:

/// 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
  1. OBJC_BOOL_IS_BOOL现在默认先判断预编译器的定义。若预编译器未定义该宏,再进行平台判断。

  2. 这里特别强调了TARGET_OS_OSX这个条件,即Mac OS操作系统下,BOOL为signed char类型。其实在早前的Xcode7.3中,TARGET_OS_OSX也是将BOOL定义为signed char,至于苹果在普遍为64位的Mac里不使用bool来作为BOOL的真实类型,还不得而知,如果你知道,可以告诉我~

  3. __ARM_ARCH_7K这个宏,应该是在Clang和LLVM中定义的。搜索LLVM4.0源码,能得到的信息只有:

    // Unfortunately, __ARM_ARCH_7K__ is now more of an ABI descriptor. The CPU
    // happens to be Cortex-A7 though, so it should still get __ARM_ARCH_7A__.
    if (getTriple().isWatchABI())
      Builder.defineMacro("__ARM_ARCH_7K__", "2");
    

    据悉iWatch的CPU指令集为armv7k,由该定义推测,__ARM_ARCH_7K应该是代表手表的宏。

想到什么写什么
Web note ad 1