Objc 中 “== YES” 的愚蠢行為有多可怕

weixin_34082695發表於2018-02-23

問題引出:
幾個星期前,我遇到一個這樣的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 中,表示真假的有 BOOLboolBoolean, 其實 boolBoolean 均是 CC++ 語言更為通用。

三者的區別:

型別 定義 標頭檔案
bool _Bool (int) stdbool.h true false
Boolean unsigned char MacTypes.h TRUE FALSE
BOOL signed char objc.h YES NO

其中,最大的區別在於 BOOL 被定義為了 signed charsigned 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位機器:


6287298-c3cce2dedd31bbde.png
32位機器.png

64位機器:


6287298-7af90d9554ffc5df.png
64位機器.png

這就解釋了上面那段程式碼在兩種不同機器上輸出結果不一致的問題了:

在 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 位機器的表現不一致。

相關文章