每一年一次的iOS升級,都會給開發者帶來一些適配工做,一些本來工做正常的代碼可能就會發生崩潰。 本文講到了一種 CoreFoundation 對象的內存管理方式在iOS13上遇到的問題。安全
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
咱們都知道蘋果使用 「引用計數」 技術來管理內存, 使用 「自動釋放池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
不是啥都沒作麼?實際上是用於這裏的優化。
瞭解了 ObjC的 autorelease優化以後,再回到咱們遇到的crash問題。有理由懷疑 [NSURL host]
這個方法在舊版本系統上不會走這個優化,所以返回值被放入了 AutoreleasePool
因此後面繼續使用是正常的。可是iOS13 上走到了這個優化邏輯,實際上返回的 host
是沒有加入 AutoreleasePool
的。 而這個時候剛好又沒有 objc 對象接收,直接用 __bridge
轉移到了 CF對象上。致使這個 host
直接釋放了。
經過查看 對 [NSURL host]
的調用代碼證實了這個猜測:
[NSURL host]
獲取host.mov x29, x29
因此若是[NSURL host]
裏的實現是相似上述 funcB
則會走到autorelease優化。也就是返回的 host 沒有加入autoreleasePool還須要證實的就是 [NSURL host]
自己的實現了。因而對比了iOS12 和 iOS13 上的實現:
iOS12 上內部經過調用了 [NSURL _cfurl]
獲取,已經加入了autoreleasePool。
在iOS13上,就是正常的取值作autorelease, 所以會走到優化邏輯:
慎用 __bridge
來進行 OC對象和 CF對象直接的強轉。 由於Autorelease優化的存在,這種用法可能讓你的代碼不安全,所以儘量使用 CFBridgeRetain
__bridge_retained
來轉換管理CF對象,避免由於做用域不一致的狀況致使對象唄提早釋放的問題。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。