進入主題以前,先請你們思考一下下面代碼的輸出git
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
@implementation Person
@end int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
Person *p1 = [p init];
Person *p2 = [p init];
CCNSLog(@"p ==> %@", p);
CCNSLog(@"p1 ==> %@", p1);
CCNSLog(@"p2 ==> %@", p2);
}
return 0;
}
複製代碼
執行的結果是:github
顯而易見,對象p、p一、p2的內存地址一致,即這三者是同一個對象。那麼問題來了,爲何這三個對象地址是同樣的?alloc
和init
底層到底作了什麼?帶着這些問題,咱們從源碼的角度探索一下吧。sass
objc4
源碼。博主用到是最新版(objc4-756.2源碼),同時,
XCode
版本是Version 11.3 (11C29)
。
源碼版本和XCode
版本不須要與博主一致~bash
下載到本地後,須要對工程進行一番編譯調試,具體步驟可參考 Cooci大佬 的博客 iOS_objc4-756.2 最新源碼編譯調試。app
編譯經過後,就能夠新建個target耍耍了。函數
博主已經把編譯好的
objc4-756.2
項目傳到 github 了,感興趣的同窗能夠下載哈~源碼分析
由於oc
語言的runtime
特性,咱們並不能確定入口必定是+alloc
方法,也就是說首先須要找到真正的入口。post
經常使用的代碼跟蹤方式:性能
Debug
->Debug Workflow
->Always show Disassembly
control
+ step into
alloc
博主經常使用第一種,無他,手熟爾優化
objc_alloc
——alloc
的真正入口給[Person alloc]
加斷點
此時,在XCode的菜單欄依次點擊Debug
->Debug Workflow
->Always show Disassembly
,獲得彙編代碼
不難發現,接下來會執行objc_alloc
。源碼以下圖:
思考:爲何
[Person alloc]
會調用objc_alloc
?(答案會在文末揭曉)
callAlloc
分析——第一次的親密接觸objc_alloc()
內部調用callAlloc()
,其源碼爲:
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
複製代碼
對callAlloc()
的分析以下:
slowpath(bool)
與fastpath(bool)
:經常使用於if-else
,能夠優化判斷的速度。// fastpath(x):表示x爲1(執行if代碼塊)的可能性更大
#define fastpath(x) (__builtin_expect(bool(x), 1))
// slowpath(x):表示x爲0(執行else代碼塊)的可能性更大
#define slowpath(x) (__builtin_expect(bool(x), 0))
複製代碼
hasCustomAWZ()
:意思是hasCustomAllocWithZone
,便是否有重寫類的+allocWithZone:
方法,可是它的值並不能簡單地這麼判斷!先看源碼bool hasCustomAWZ() {
return ! bits.hasDefaultAWZ();
}
複製代碼
注意:
hasCustomAWZ()
的值問題
- 類的
+initialize:
方法主要用於初始化靜態變量。在其執行以前,hasDefaultAWZ()
值爲false
,即hasCustomAWZ()
爲true
;其執行以後,若是當前類重寫了+allocWithZone:
方法,hasCustomAWZ()
爲true
,不然爲false
。- 類的
+initialize:
方法會在第一次初始化該類以前調用。當調用[cls alloc]
時,會觸發objc_msgSend
,而後會執行+initialize:
。(感興趣的同窗能夠分別打印+alloc
和+initialize:
方法加以驗證)
所以,當類第一次來到callAlloc()
時,最終會執行[cls alloc]
。
canAllocFast()
源碼以下:bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
複製代碼
再往底層找bits.canAllocFast()
,發現關鍵宏FAST_ALLOC
#if FAST_ALLOC
...
bool canAllocFast() {
return bits & FAST_ALLOC;
}
#else
...
bool canAllocFast() {
return false;
}
#endif
複製代碼
繼續深刻,來到了FAST_ALLOC
宏定義之處
#if !__LP64__ // 當前操做系統不是64位
...
#elif 1 // 當前操做系統是64位
...
#else
...
#define FAST_ALLOC (1UL<<2)
...
#endif
複製代碼
從上面宏代碼能夠得出這樣的結論,即不管當前操做系統是否是64位,都沒有定義FAST_ALLOC
,也就是說,canAllocFast()
永遠是false
!
所以,若是hasCustomAWZ()
爲false
時,會直接去到class_createInstance()
。
alloc
->_objc_rootAlloc
->callAlloc
->class_createInstance
經過對hasCustomAWZ()
的分析,咱們知道類的第一次初始化最終是走到callAlloc
的最後,即return [cls alloc];
[cls alloc]
,此次真的來到alloc()
方法了+ (id)alloc {
return _objc_rootAlloc(self);
}
複製代碼
_objc_rootAlloc()
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
複製代碼
callAlloc()
->class_createInstance()
再次來到callAlloc
,此時hasCustomAWZ()
的值取決於當前類是否重寫了+allocWithZone:
方法。
因爲Person
類沒有重寫,fastpath(!cls->ISA()->hasCustomAWZ())
爲true,而canAllocFast()
永遠爲false
。
所以,接下來會走到class_createInstance()
,其源碼以下:
id class_createInstance(Class cls, size_t extraBytes) {
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
複製代碼
_class_createInstanceFromZone
顧名思義,這是要建立對象!可是,alloc
的時候就建立對象???如今,咱們暫時把疑問放下,先分析一下源碼:
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// 一次讀取類的信息位以提升性能
bool hasCxxCtor = cls->hasCxxCtor(); // 是否有構造函數
bool hasCxxDtor = cls->hasCxxDtor(); // 是否有析構函數
bool fast = cls->canAllocNonpointer();
// 計算內存
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
// 分配1塊大小爲size的連續內存
obj = (id)calloc(1, size);
if (!obj) return nil;
// 初始化對象的isa
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
複製代碼
對_class_createInstanceFromZone()
的分析以下:
instanceSize(extraBytes)
計算內存,此時的extraBytes
是0
,其源碼是size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
複製代碼
WORD_MASK
在64
位操做系統下是7
,不然是3
,所以,word_align()
在64
位系統下是8字節對齊
,其餘位系統下是4字節對齊
。
instanceSize()
函數同時對內存大小又進行了最小16字節
的限制。
canAllocNonpointer()
是對isa的類型的區分,在 __OBJC2__
中,若是一個類使用isa_t
類型的isa
的話,fast
就是true
;而在__OBJC2__
中,zone
會被忽略,因此!zone
也是true
;綜上,接着就是calloc()
和initInstanceIsa()
。
calloc()
的底層源碼是在 蘋果開源的libmalloc 中,通過調試,calloc
分配的內存大小受segregated_size_to_fit()
影響,看下面源碼:static MALLOC_INLINE size_t segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey) {
size_t k, slot_bytes;
if (0 == size) {
// Historical behavior
size = NANO_REGIME_QUANTA_SIZE;
}
// round up and shift for number of quanta
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
// multiply by power of two quanta size
slot_bytes = k << SHIFT_NANO_QUANTUM;
// Zero-based!
*pKey = k - 1;
return slot_bytes;
}
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
複製代碼
其中,slot_bytes
至關於(size + 16-1) >> 4 << 4
,也就是16字節對齊
。
initInstanceIsa()
就是初始化isa
,而且關聯cls
。isa 是 objc 類結構中極其重要的一環,關於它的結構、初始化過程、繼承關係等內容,博主會另起一篇文章講述,敬請期待。
從上面的代碼能夠看出,_class_createInstanceFromZone()
作了不少事情,而且最終確實建立了對象,幾乎幹了全部事情,那麼,init
又到底作了什麼呢?請接着看下去。
init
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
複製代碼
很是簡單,init
僅僅是將alloc
建立的對象返回。爲何這樣設計呢?其實並不難理解,在平時的開發中,咱們經常會根據業務需求重寫init
,進行一些自定義的配置。
NSObject
的init
是一種工廠設計方案,方便子類重寫。
new
咱們再看看new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
複製代碼
很明顯,new
至關於alloc
+init
。
關於alloc
、init
以及new
的源碼分析就到這了。在alloc
的過程當中,callAlloc
和_class_createInstanceFromZone
這兩個函數是重點。
以上源碼流程分析,是創建在
objc4-756.2
源碼的基礎上的,756.2
是目前最新的版本。
下面用流程圖總結一下alloc
建立對象的過程
以上就是OC對象源碼建立的所有內容了。回首整個過程,有順利也有坎坷,整體比較燒腦,可是通過alloc
這一條龍服務後,彷彿完成了某項重任,身心無比愉悅。
OC源碼分析之路,必將是榮譽之路,但願你們且行且珍惜,你我共勉!
[Person alloc]
會調用objc_alloc
?_read_images()
函數中,有這樣一段代碼:void _read_images(...)
{
...
#if SUPPORT_FIXUP
// Fix up old objc_msgSend_fixup call sites
for (EACH_HEADER) {
message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
if (count == 0) continue;
...
for (i = 0; i < count; i++) {
fixupMessageRef(refs+i);
}
}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
...
}
複製代碼
而在fixupMessageRef()
中,有對SEL_alloc
進行IMP
的修復綁定
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == SEL_allocWithZone) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == SEL_retain) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == SEL_release) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == SEL_autorelease) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
}
}
...
}
複製代碼
經過[Person alloc]
調用的是objc_alloc()
這個既定事實,咱們能夠猜想,在項目編譯生成Mach-O
文件期間,造成了SEL_alloc
與objc_alloc
的對應關係。
你們能夠將編譯生成的
Mach-O
文件拖到MachOView
中,驗證一下,看看可否找到objc_alloc
alloc
,底層流程區別?若是Person
類重寫了+allocWithZone:
呢?Person *p1 = [Person alloc];
Person *p2 = [Person alloc];
複製代碼
你們能夠本身試試,經過比較會幫助你們理解記憶
alloc
的流程。