OC源碼分析之對象的建立

前言

拋出問題

進入主題以前,先請你們思考一下下面代碼的輸出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的內存地址一致,即這三者是同一個對象。那麼問題來了,爲何這三個對象地址是同樣的?allocinit底層到底作了什麼?帶着這些問題,咱們從源碼的角度探索一下吧。sass

準備工做

  1. 蘋果官方開源代碼列表 找到 objc4源碼。

博主用到是最新版(objc4-756.2源碼),同時,XCode版本是Version 11.3 (11C29)
源碼版本和XCode版本不須要與博主一致~bash

  1. 下載到本地後,須要對工程進行一番編譯調試,具體步驟可參考 Cooci大佬 的博客 iOS_objc4-756.2 最新源碼編譯調試app

  2. 編譯經過後,就能夠新建個target耍耍了。函數

博主已經把編譯好的objc4-756.2項目傳到 github 了,感興趣的同窗能夠下載哈~源碼分析

1. alloc源碼分析

由於oc語言的runtime特性,咱們並不能確定入口必定是+alloc方法,也就是說首先須要找到真正的入口。post

經常使用的代碼跟蹤方式:性能

  1. XCode菜單欄依次點擊Debug->Debug Workflow->Always show Disassembly
  2. control + step into
  3. 下符號斷點,如alloc

博主經常使用第一種,無他,手熟爾優化

1.1 objc_alloc——alloc的真正入口

[Person alloc]加斷點

此時,在XCode的菜單欄依次點擊Debug->Debug Workflow->Always show Disassembly,獲得彙編代碼

不難發現,接下來會執行objc_alloc。源碼以下圖:

思考:爲何[Person alloc]會調用objc_alloc?(答案會在文末揭曉)

1.2 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()的分析以下:

  1. 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))
複製代碼
  1. 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]

  1. 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()

1.3 alloc->_objc_rootAlloc->callAlloc->class_createInstance

經過對hasCustomAWZ()的分析,咱們知道類的第一次初始化最終是走到callAlloc的最後,即return [cls alloc];

  1. 因爲執行了[cls alloc],此次真的來到alloc()方法了
+ (id)alloc {
    return _objc_rootAlloc(self);
}
複製代碼
  1. 接着是_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*/);
}
複製代碼
  1. 而後是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);
}
複製代碼

1.4 _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()的分析以下:

  1. instanceSize(extraBytes)計算內存,此時的extraBytes0,其源碼是
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_MASK64位操做系統下是7,不然是3,所以,word_align()64位系統下是8字節對齊,其餘位系統下是4字節對齊

instanceSize()函數同時對內存大小又進行了最小16字節的限制。

  1. canAllocNonpointer()是對isa的類型的區分,在 __OBJC2__ 中,若是一個類使用isa_t類型的isa的話,fast就是true;而在__OBJC2__中,zone會被忽略,因此!zone也是true

綜上,接着就是calloc()initInstanceIsa()

  1. 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字節對齊

  1. initInstanceIsa()就是初始化isa,而且關聯cls

isa 是 objc 類結構中極其重要的一環,關於它的結構、初始化過程、繼承關係等內容,博主會另起一篇文章講述,敬請期待。

從上面的代碼能夠看出,_class_createInstanceFromZone()作了不少事情,而且最終確實建立了對象,幾乎幹了全部事情,那麼,init又到底作了什麼呢?請接着看下去。

2. init和new

1. 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,進行一些自定義的配置。

NSObjectinit是一種工廠設計方案,方便子類重寫。

2. new

咱們再看看new

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
複製代碼

很明顯,new至關於alloc+init

3. 總結

關於allocinit以及new的源碼分析就到這了。在alloc的過程當中,callAlloc_class_createInstanceFromZone這兩個函數是重點。

以上源碼流程分析,是創建在objc4-756.2源碼的基礎上的,756.2是目前最新的版本。

下面用流程圖總結一下alloc建立對象的過程

4. 結束語

以上就是OC對象源碼建立的所有內容了。回首整個過程,有順利也有坎坷,整體比較燒腦,可是通過alloc這一條龍服務後,彷彿完成了某項重任,身心無比愉悅。

OC源碼分析之路,必將是榮譽之路,但願你們且行且珍惜,你我共勉!

補充

  1. Q:思考:爲何[Person alloc]會調用objc_alloc
    A:項目編譯的時候,會讀取鏡像文件,在_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_allocobjc_alloc的對應關係。

你們能夠將編譯生成的Mach-O文件拖到MachOView中,驗證一下,看看可否找到objc_alloc

最後的問題

  1. 下面的兩次alloc,底層流程區別?若是Person類重寫了+allocWithZone:呢?
Person *p1 = [Person alloc];
Person *p2 = [Person alloc];
複製代碼

你們能夠本身試試,經過比較會幫助你們理解記憶alloc的流程。

相關文章
相關標籤/搜索