目錄
前言
為什么不按源碼流程調用?
alloc的調用流程
前言
在之前的博客中我們有學習到過alloc的底層原理,沿著源碼一步步找到了alloc的調用鏈——alloc—>_objc_rootAlloc—>callAlloc—>_objc_rootAllocWithZone—>_class_createInstanceFromZone,但其實在實際的alloc過程中,并不是這個調用流程,如果對NSObject的alloc加上斷點調試就會發現,alloc流程并沒有進入源碼,接下來我們來探究一下為什么會這樣以及真實的調用流程。
為什么不按源碼流程調用?
在實際運行中,
[NSObject alloc]
或[MyClass alloc]
這類調用通常不會真正進入 libobjc 的源碼層(比如_objc_rootAlloc
),而是走了更加高效的底層路徑,這是由于 Apple 對運行時做了大量優化(比如匯編級別快速路徑、ISA-optimized fast path)來避免頻繁進入 C 層函數。
1.對于非NSObject類, objc_msgSend是匯編函數,非普通 C 函數
-
它大多數時候在匯編層 直接查找 IMP 并跳轉執行,不會進入 Objective-C runtime 的 C 函數實現。
-
也就是說,objc_msgSend(obj, @selector(alloc)) 通常直接跳到了元類中的 +alloc 的 IMP。(這里涉及到后面cache_t的方法緩存,如果命中了Cache,就不會走完整的流程)
-
對于NSObject, 是基礎類,系統做了特殊優化
-
對于 NSObject和一些常見類,Apple 使用了 匯編級別的 fast path,這意味著即便你打斷點試圖進入 _objc_rootAlloc,你可能根本進不去。
-
常見的“未命中源碼”的情況說明使用的是緩存或特殊入口。
alloc的調用流程
通過調用alloc后的堆棧詳情我們就可以發現,無論是NSObject類還是自定義類,調用alloc方法最開始走的都是objc_alloc而不是objc_rootAlloc。這是因為消息轉發時系統在底層幫我們轉發到了objc_alloc。我們來看看objc_alloc的源碼實現
可以發現其實他和objc_rootAlloc的實現是一樣的,調用callAlloc。
回顧之前callAlloc的實現
可以看到callAlloc中分為幾個分支來處理
對于NSObjcet類,初始化在llvm編譯時就已經初始化好了,因此緩存中已經有alloc/allocWithZone方法了,hasCustomAWZ()為false,那么!cls->ISA()->hasCustomAWZ()就為true。
因此NSObjcet在此時會進入_objc_rootAllocWithZone并調用_class_createInstanceFromZone,后面的步驟就和之前說的一樣了,這就是為什么NSObjct沒有走alloc方法。
而對于自定義類,初次創建時沒有默認的alloc/allocWithZone實現,所以繼續向下執行進入到消息發送流程,消息轉發時會向父類找,最終找到NSObjcet的alloc并調用,即[NSObjcet alloc],這時會來到_objc_rootAlloc,進入后再次調用callAlloc,這次調用的是NSObject類的,緩存中存在alloc/allocWithZone實現,就接著走_objc_rootAllocWithZone方法,后面步驟也就和之前一樣了。所以自定義類在調用alloc時會走兩次callAlloc。
總結一下,NSObjcet的調用鏈如下圖:
自定義類的調用鏈如下圖: