iOS13 一次Crash定位 - 被釋放的NSURL.host

每一年一次的iOS升級,都會給開發者帶來一些適配工做,一些本來工做正常的代碼可能就會發生崩潰。 本文講到了一種 CoreFoundation 對象的內存管理方式在iOS13上遇到的問題。安全

 

1. 問題

iOS 13 Beta 版本上,手淘出現了一個必現的崩潰:app

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libobjc.A.dylib                 0x00000001d6f9af20 objc_retain + 16
1   CFNetwork                       0x00000001d7843f60 0x1d77b0000 + 606048
2   CFNetwork                       0x00000001d780cec8 0x1d77b0000 + 380616
3   CFNetwork                       0x00000001d77dff24 _CFSocketStreamCreatePair + 56
4   xxxxxxxxxxxxxxxxx               0x000000010c2a44b4 0x10b46c000 + 14910644
5   xxxxxxxxxxxxxxxxx               0x000000010c2a6238 0x10b46c000 + 14918200
6   xxxxxxxxxxxxxxxxx               0x000000010c2a661c 0x10b46c000 + 14919196

崩潰在了 _CFSocketStreamCreatePair  方法裏面, 而後崩潰在了 objc_retain  裏面,推測是傳入的某個ObjC的對象野指針了致使的。優化

經過追溯源碼,發現調用的是 CFStreamCreatePairWithSocketToHost 這個方法,而後找到這個方法的定義:ui

void CFStreamCreatePairWithSocketToHost(
    CFAllocatorRef _Null_unspecified alloc, 
    CFStringRef _Null_unspecified host, 
    UInt32 port,
    CFReadStreamRef _Null_unspecified * _Null_unspecified readStream, 
    CFWriteStreamRef _Null_unspecified * _Null_unspecified writeStream
);

根據上下文判斷,是第二個參數 CFStringRef _Null_unspecified host  野指針了。url

而後找到這個 host 對象的初始化:spa

NSURL *serverUrl = [NSURL URLWithString:@"xxxxx"];
CFStringRef hostRef = (__bridge CFStringRef)serverUrl.host;

這段代碼看起來好像並無問題,怎麼會致使野指針,而後Crash呢?線程

這要從iOS的內存管理上找答案。3d

 

2. 蘋果的autorelease內存管理優化

咱們都知道蘋果使用 「引用計數」 技術來管理內存, 使用 「自動釋放池AutoreleasePool」 技術來解決方法返回值的內存管理問題。 相關技術原理網上都有不少文章。可是本文中遇到的Crash是由蘋果對使用 ARC 代碼進行的編譯優化從而引起的。因此先講一下這個優化是什麼。指針

考慮一個內存管理的最簡單的case:調試

在最初的 ARC 機制下,上圖中的左邊代碼會編譯成右邊這樣的代碼,從而保證了對象 b 的生命週期完整。

可是咱們再詳細分析下這個代碼,是否是去掉 [b autorelease]  和 [b retain] 這兩步操做的話,代碼也是能夠正常執行的呢? 答案是確定的, 那麼這個操做其實就是能夠優化掉的。蘋果考慮到了這一點。

那麼要怎麼樣作到這個優化呢? 由於這個優化是須要同時考慮 被調用方funcB 和 調用方funcA 這兩個方法配合來完成,由於須要根據調用方的內存管理代碼才能決定我被調用方要不要真的去掉autorelease操做。 並且還要在ABI上向下適配。 蘋果是這樣作的:

代碼:

// Prepare a value at +1 for return through a +0 autoreleasing convention.
id 
objc_autoreleaseReturnValue(id obj)
{
    // 判斷是否須要優化, 若是能夠,就直接return,不作autorelease
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

id
objc_retainAutoreleasedReturnValue(id obj)
{
    // 判斷是否走了優化邏輯,若是走了就不用retain
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
    assert(getReturnDisposition() == ReturnAtPlus0);
    // 判斷方法返回地址是否是某個值,是的話就認爲能夠優化
    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        // 能夠優化就把ReturnAtPlus1 存起來,存到了tls裏面
        if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false;
}

static ALWAYS_INLINE bool 
callerAcceptsOptimizedReturn(const void *ra)
{
    // fd 03 1d aa    mov fp, fp
    // arm64 instructions are well-aligned
    // 判斷return address是否是 0xaa1d03fd, 在arm64上就是 `mov fp, fp` 指令
    if (*(uint32_t *)ra == 0xaa1d03fd) {
        return true;
    }
    return false;
}

static ALWAYS_INLINE ReturnDisposition 
acceptOptimizedReturn()
{
    ReturnDisposition disposition = getReturnDisposition();
    setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state
    return disposition;
}

// 存在當 tls中,當前線程相關的
static ALWAYS_INLINE ReturnDisposition 
getReturnDisposition()
{
    return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}

static ALWAYS_INLINE void 
setReturnDisposition(ReturnDisposition disposition)
{
    tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}

從上面的分析中,咱們能夠得出,只要看到調用 objc_msgSend 以後的一條指令是 mov x29, x29 , 那麼確定就是開啓了這個優化。

因此,你們彙編調試的時候看到這樣一行指令,不要以爲奇怪 mov x29,x29 不是啥都沒作麼?實際上是用於這裏的優化。

3. Crash根因

瞭解了 ObjC的 autorelease優化以後,再回到咱們遇到的crash問題。有理由懷疑 [NSURL host] 這個方法在舊版本系統上不會走這個優化,所以返回值被放入了 AutoreleasePool 因此後面繼續使用是正常的。可是iOS13 上走到了這個優化邏輯,實際上返回的 host 是沒有加入 AutoreleasePool 的。 而這個時候剛好又沒有 objc 對象接收,直接用 __bridge 轉移到了 CF對象上。致使這個 host 直接釋放了。

經過查看 對 [NSURL host] 的調用代碼證實了這個猜測:

  1. +312 行調用 [NSURL host] 獲取host.
  2. 由於 +316的指令是 mov x29, x29  因此若是[NSURL host]  裏的實現是相似上述 funcB 則會走到autorelease優化。也就是返回的 host 沒有加入autoreleasePool
  3. +320 行中,由於開啓優化,也捕獲作retain
  4. +328 行,直接release,  這個時候 host就釋放了
  5. 後續繼續對它進行訪問,就Crash了。

還須要證實的就是 [NSURL host]自己的實現了。因而對比了iOS12 和 iOS13 上的實現:

iOS12 上內部經過調用了 [NSURL _cfurl] 獲取,已經加入了autoreleasePool。

在iOS13上,就是正常的取值作autorelease, 所以會走到優化邏輯:

4. 小結

慎用 __bridge 來進行 OC對象和 CF對象直接的強轉。 由於Autorelease優化的存在,這種用法可能讓你的代碼不安全,所以儘量使用 CFBridgeRetain  __bridge_retained 來轉換管理CF對象,避免由於做用域不一致的狀況致使對象唄提早釋放的問題。


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索