iOS-OC底層一:對(duì)象alloc的本質(zhì)

1.準(zhǔn)備源碼程序

源碼分析alloc&init&new的流程,使用從github上下載的LGCooci的源碼https://github.com/LGCooci/KCCbjc4_debug

因?yàn)樵O(shè)備限制,我是基于818的源碼進(jìn)行學(xué)習(xí)。

從github下載完成后,在KCObjcBuild所在的目錄新建一個(gè)OC類命名為Person,Person類中什么都不寫。

在main.m中寫入如下代碼:

#import "Person.h"

Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@ - %p - %p",p1,p1,&p1);
NSLog(@"%@ - %p - %p",p2,p2,&p2);
NSLog(@"%@ - %p - %p",p3,p3,&p3);

選擇target為KCObjcBuild,就可以運(yùn)行了。


運(yùn)行結(jié)果.png

指針在棧上開辟的地址,指針是int類型,64位機(jī)上大小為8字節(jié)沒有問題。但是MAC上與iOS上開辟的順序有一點(diǎn)區(qū)別,在iOS上P1、P2、P3的地址依次減小8;Mac上卻是P1、P3、P2的順序依次減小8。

執(zhí)行的結(jié)果分析:

  • 在OC中alloc用來分配內(nèi)存空間,init進(jìn)行初始化。上面代碼中只分配了一個(gè)Person對(duì)象的內(nèi)存空間,聲明了3個(gè)指針P1、P2、P3,都指向了同一個(gè)聲明的Person對(duì)象。

  • %@打印指針對(duì)象P1、P2、P3,打印的是他們所指向的對(duì)象,也就是那同一份Person對(duì)象,輸出<Person: 0x100698e90>

  • %p打印指針對(duì)象P1、P2、P3,%p是打印地址,P1、P2、P3指向?qū)ο蠖际?strong><Person: 0x100698e90>,打印的是Person對(duì)象的地址,所以打印的都是0x100698e90

  • %p打印指針&P1、&P2、&P3,P1、P2、P3是指針,&取地址符是指指針本身的地址,指針存放在棧上,所以打印的是它們?cè)跅I系牡刂贰?/p>

執(zhí)行結(jié)果分析圖解.png

那么alloc和init具體做了什么呢?

2.用源碼分析alloc創(chuàng)建過程

2.1修復(fù)斷點(diǎn)不走問題

Person *p1 = [Person alloc]那一行,打一個(gè)斷點(diǎn)。運(yùn)行,會(huì)出現(xiàn)斷點(diǎn)無(wú)效。

如果遇到斷點(diǎn)無(wú)效的問題,確保如下兩步是正確的:

  1. Build PhasesCompile Sources中,將main.m拖到最前面
  2. 找到Targets -> Build Settings -> Enable Hardened Runtime,值置為NO

2.2跟蹤源碼

直接step into跟蹤源碼。

2.2.1先走NSObject.mm+ (id)alloc方法

+ (id)alloc {
    return _objc_rootAlloc(self);
}

2.2.2再走NSObject.mmid _objc_rootAlloc(Class cls)方法

// 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*/);
}

2.2.3再走NSObject.mmcallAlloc方法

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

這個(gè)方法走的是上面那部分:

#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

objective-c2.0是2006年蘋果發(fā)布版本,但是至此之后沒有再重新命名objective-c3.0,雖然官方的源代碼已經(jīng)以O(shè)BJC4命名,但是項(xiàng)目中依然是OBJC2。

出于好奇,到底OBJC2是什么玩意。沒有找到相關(guān)的宏定義。

看到代碼中有如下一處#if !defined(__cplusplus) && !__OBJC2__,便打印一下這其中到底是什么值:

分析宏定義的值.png

__cplusplus定義編程語(yǔ)言和編譯器之間的關(guān)系。

199711L(until C++11), 201103L(C++11), 201402L(C++14), 201703L(C++17), or 202002L(C++20)

UNAVAILABLE_ATTRIBUTE:告知方法失效。

關(guān)于fastpathslowpath,雖然不重要,但是還是了解一下:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
  • C++中的__builtin_expect(),這個(gè)指令是gcc引入的,作用是"允許程序員將最有可能執(zhí)行的分支告訴編譯器"。這個(gè)指令的寫法為:__builtin_expect(EXP, N)。意思是:EXP==N的概率很大。__builtin_expect是為了生成高效的代碼。

  • fastpath定義中__builtin_expect((x),1)表示x 的值為真的可能性更大;即執(zhí)行if里面語(yǔ)句的機(jī)會(huì)更大。

  • slowpath定義中的__builtin_expect((x),0)表示x 的值為假的可能性更大。即執(zhí)行else里面語(yǔ)句的機(jī)會(huì)更大

  • 在日常的開發(fā)中,也可以通過設(shè)置來優(yōu)化編譯器,達(dá)到性能優(yōu)化的目的,設(shè)置的路徑為:Build Setting--> Optimization Level--> Debug-->None改為fastest或者smallest

2.2.4再走objc-runtime-new.mm_objc_rootAllocWithZone方法

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

2.2.5再走objc-runtime-new.mm_class_createInstanceFromZone方法

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);//將內(nèi)存空間與類關(guān)聯(lián)
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

2.2.6 alloc重點(diǎn)方法分析

很明顯_class_createInstanceFromZone是我們要重點(diǎn)研究的對(duì)象,首先思考:

  • 要開辟多少內(nèi)存
  • 怎么取申請(qǐng)內(nèi)存
  • ISA如何跟內(nèi)存進(jìn)行綁定

對(duì)應(yīng)在代碼中

  • cls->instanceSize:計(jì)算需要開辟的內(nèi)存空間大小
  • calloc申請(qǐng)內(nèi)存,返回地址指針(Zone已經(jīng)廢棄)
  • obj->initInstanceIsa:將 類 與 isa 關(guān)聯(lián)
2.2.6.1 instanceSize分析

instanceSize方法在objc-runtime-new.h中,查看源碼,斷點(diǎn)調(diào)試

inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }

    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

通過斷點(diǎn),會(huì)執(zhí)行到cache.fastInstanceSize方法,快速計(jì)算內(nèi)存大小。也就是執(zhí)行cache.fastInstanceSize(extraBytes),繼續(xù)跟進(jìn)去:

size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));

    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}

跟進(jìn)去后發(fā)現(xiàn)會(huì)走到第10行,align16(size + extra - FAST_CACHE_ALLOC_DELTA16);,繼續(xù)往下跟:

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

align16是16字節(jié)對(duì)齊算法。

內(nèi)存字節(jié)對(duì)齊原則

  • 數(shù)據(jù)成員對(duì)齊規(guī)則:struct 或者 union 的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大?。ㄖ灰摮蓡T有子成員,比如數(shù)據(jù)、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機(jī)中是4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ))
  • 數(shù)據(jù)成員為結(jié)構(gòu)體:如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ)(例如:struct a里面存有struct b,b里面有char、int、double等元素,則b應(yīng)該從8的整數(shù)倍開始存儲(chǔ))
  • 結(jié)構(gòu)體的整體對(duì)齊規(guī)則:結(jié)構(gòu)體的總大小,即sizeof的結(jié)果,必須是其內(nèi)部做大成員的整數(shù)倍,不足的要補(bǔ)齊

為什么需要16字節(jié)對(duì)齊

  • 各種數(shù)據(jù)類型長(zhǎng)短不一,需要統(tǒng)一長(zhǎng)度,在磁盤讀取時(shí),取出固定長(zhǎng)度數(shù)據(jù)。
2.2.6.2 calloc:申請(qǐng)內(nèi)存,返回匿名指針

通過instanceSize計(jì)算的內(nèi)存大小,向內(nèi)存中申請(qǐng) 大小 為 size的內(nèi)存,并賦值給obj,因此 obj是指向內(nèi)存地址的指針

2.2.6.3 obj->initInstanceIsa:類與isa關(guān)聯(lián)

經(jīng)過calloc可知,內(nèi)存已經(jīng)申請(qǐng)好了,類也已經(jīng)傳入進(jìn)來了,接下來就需要將 類與 地址指針 即isa指針進(jìn)行關(guān)聯(lián)。主要過程就是初始化一個(gè)isa指針,并將isa指針指向申請(qǐng)的內(nèi)存地址,在將指針與cls類進(jìn)行關(guān)聯(lián)。

2.2.6.4 總結(jié)
  • 通過對(duì)alloc源碼的分析,可以得知alloc的主要目的就是開辟內(nèi)存,而且開辟的內(nèi)存需要使用16字節(jié)對(duì)齊算法,現(xiàn)在開辟的內(nèi)存的大小基本上都是16的整數(shù)倍
  • 開辟內(nèi)存的核心步驟有3步:計(jì)算 -- 申請(qǐng) -- 關(guān)聯(lián)
alloc分配內(nèi)存的流程圖.png

2.2.7 init方法

工廠方法,用于重寫,自定義初始化內(nèi)容。

+ (id)init {
    return (id)self;
}

- (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;
}

2.2.8 new方法

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

通過 new 的源碼可以看出,其實(shí)它也是調(diào)用了 [callAlloc init] 方法;
但是我們推薦使用 [alloc init] 方法,因?yàn)檫@樣我們可以自定義 init 方法,使我們的開發(fā)更加的靈活。

3.在打斷點(diǎn)后重新進(jìn)行分析

真正運(yùn)行的時(shí)候會(huì)發(fā)現(xiàn)在callAlloc方法會(huì)進(jìn)入兩次,先走objc_msgSend,再走_(dá)objc_rootAllocWithZone。這里涉及到另外一個(gè)知識(shí)點(diǎn),這個(gè)問題可以通過LLVM源碼分析得到答案,留在以后研究。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容