問題引出:
幾個星期前,我遇到一個這樣的bug,在我的機器上用 debug 環境編譯出來的正常運行,但是 RDM 運行出來的總是出現錯誤。當時排查到的問題代碼大致如下:
- (void)tableFootLoadingViewDidTriggerLoading:(MQZoneTableFootLoadingView *)footLoadingView
{
[self performSelector:@selector(loadMoreData:) withObject:@(YES) afterDelay:1];
}
- (void)loadMoreData:(BOOL)isRefresh
{
if (isRefresh == YES)
{
//...
}
else
{
//...
}
}
大致的排查 bug 情況是我發現無論如何,從 performSelector
進入到的 loadMoreData
的時候,參數 isRefresh
永遠是 NO。
問題解決:
當時,我猜測,這里 @(YES) 發生了一次把 YES 轉換為 NSNumber
, 然后進入到 loadMoreData
的時候做了一層隱式轉換,變成了 BOOL
類型,并且,這層轉換對于我們來說是一個黑盒子。所以,這里出錯的可能性極大。
另外, isRefresh
參數和 YES
進行直接比較,這里的代碼似乎有點問題。通過修改這兩處地方,bug 得到了很好的解決,修改后的代碼:
- (void)tableFootLoadingViewDidTriggerLoading:(MQZoneTableFootLoadingView *)footLoadingView
{
[self performSelector:@selector(loadMoreData:) withObject:@(0) afterDelay:1];
}
- (void)loadMoreData:(NSNumber *)refreshNum
{
BOOL isRefresh = [refreshNum integerValue] != 0;
if (isRefresh)
{
}
else
{
}
}
這里,我修改了兩個地方。
1、參數由 BOOL
改為 NSNumber
, 去除了那層對我們不可見的隱式轉換
2、取消了 isRefresh == YES
的代碼,改為 if (isRefresh)
問題分析:
在 Objc 中,表示真假的有 BOOL
、bool
、Boolean
, 其實 bool
與 Boolean
均是 C
與 C++
語言更為通用。
三者的區別:
類型 | 定義 | 頭文件 | 真 | 假 |
---|---|---|---|---|
bool | _Bool (int) | stdbool.h | true | false |
Boolean | unsigned char | MacTypes.h | TRUE | FALSE |
BOOL | signed char | objc.h | YES | NO |
其中,最大的區別在于 BOOL
被定義為了 signed char
,signed char
的取值范圍為 -127~128。
一:== YES
導致問題
- 測試環境 Xcode 9.1:
下面代碼輸出了 NO:
int main(int argc, char * argv[])
{
if (2 == YES)
{
NSLog(@"YES");
}
else
{
NSLog(@"NO");
}
}
下面的代碼輸出 YES
int main(int argc, char * argv[])
{
if (2)
{
NSLog(@"YES");
}
else
{
NSLog(@"NO");
}
}
第二段代碼輸出 YES
是很顯然的,但是第一段代碼為何輸出了 NO
, 為此,我們可以輸出 YES, 看結果是啥
NSLog(@"%d", YES); //結果輸出了 1
所以,答案是顯而易見的,2 怎么可能 == 1 呢,所以 這里的第一段代碼輸出了 1。
二:不同機型上的問題
- 測試環境 Xcode 9.1, iPhone 5(注意 5s 為 64位) 與 iPhone 6 模擬器:
下面的代碼在 32 位機器上 NO, 64 位機器上輸出 YES
int main(int argc, char * argv[])
{
BOOL result = 2;
if (result == YES)
{
NSLog(@"YES");
}
else
{
NSLog(@"NO");
}
}
下面代碼在 32 位與 64 位機器中,均輸出 YES
int main(int argc, char * argv[])
{
BOOL result = 2;
if (result)
{
NSLog(@"YES");
}
else
{
NSLog(@"NO");
}
}
第二個結果明顯是正確的,但是第一個又是為什么產生差異呢?
讓我們看看 YES 的定義:
#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
首先是宏 __has_feature(objc_bool)
, 通過下面的代碼
#if __has_feature(objc_bool)
NSLog(@"YES = __objc_yes");
#else
NSLog(@"YES = 1");
#endif
我發現 32 位 和 64 位機器,都運行了 NSLog(@"YES = __objc_yes");
,也就是說 32 位 和 64 位 YES 都被定義為了 __objc_yes
很遺憾,我沒有找到 __objc_yes
的定義,但是我們可以簡單的把它打印出來看看結果,
NSLog(@"%d", __objc_yes);
輸出結果均為 1
但是,我們通過編譯器的警告,可以看到 __objc_yes
在 32 位和 64 位機器的不同:
32位機器:
64位機器:
這就解釋了上面那段代碼在兩種不同機器上輸出結果不一致的問題了:
在 64 位機器上, __objc_yes
就是 bool 類型的某一個值,那么在 C++ 中,任何非 0 的值就是 true,所以,在 64 位機器上,result == YES
的代碼能夠順利執行。
但是在 32 位機器上,__objc_yes
是一個 signed char
,并且 = 1,2 == 1 這個邏輯顯然過不去,所以這里會導致 32 位和 64 位代碼的不同運行結果。
但是,到了這里,我好奇一點:在 64 位機器上,為何 (2 == YES)
無法通過 但是 result = 2; result == YES
卻可以通過呢?
于是,我運行了下面代碼
BOOL result = 2;
NSLog(@"%d", result);
上述代碼在 32 位機器上輸出了 2, 在 64 位機器上輸出了 YES, 這也就解釋了上面的問題,也就是說,真正起作用的其實是 BOOL = int 這一層隱式轉換。這一層,對我們來說是黑盒子,而且在 64 位與 32 位機器的表現不一致。