OC對象底層探索(本質(zhì)、創(chuàng)建流程、內(nèi)存對齊及空間大小)

目錄

作為一個開發(fā)者,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:196800191,加群密碼:112233,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長!

1. 概述

每個iOS開始人員對OC語言并不陌生,雖然現(xiàn)在蘋果提倡swift開發(fā),但是OC還是入門的必修課,平時開發(fā)的時候,我們通常就是調(diào)用各種API,很少探究其底層的原理,蘋果是如何在底層進(jìn)行封裝的呢?作為入行幾年的開發(fā)者,還是有必要一探究竟。
OC是面向?qū)ο蟮恼Z言,在代碼中最常見的就是創(chuàng)建一個對象了,那么對象是什么,底層的結(jié)構(gòu)又是什么呢,是如何創(chuàng)建出來的呢?帶著這些問題,我們來開始分析。

2. 對象是什么

對象在底層到底是個什么樣子呢?
在項(xiàng)目中創(chuàng)建一個GYMPerson類,里面定義個name屬性和一個成員變量hobby,如下:

@interface GYMPerson : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@end

然后通過命令行將main.m文件轉(zhuǎn)成c++文件main.cpp.

clang -rewrite-objc main.m -o main.cpp

轉(zhuǎn)換完成后打開main.cpp文件,此時找到了:

struct GYMPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *hobby;
    NSString *_name;
};

這個就是GYMPerson在底層的形式,一個結(jié)構(gòu)體,并且繼承了父類的所有屬性。
另外我們注意到hobby沒有下劃線,而name則有下劃線,我們都知道成員變量在底層保持不變,不會生成一個帶下劃線的成員變量的,而name是一個屬性,在底層是會生成一個帶下劃線的成員變量的,而且還會增加getter和setter方法,如下:

static NSString * _I_GYMPerson_name(GYMPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GYMPerson$_name)); }
static void _I_GYMPerson_setName_(GYMPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GYMPerson, _name), (id)name, 0, 1); }

所以對象的本質(zhì)是什么?毫無疑問,結(jié)構(gòu)體。

3. 對象創(chuàng)建流程

我們在代碼中調(diào)用alloc方法創(chuàng)建對象的時候,通常會經(jīng)歷以下幾個步驟,簡易圖如下:



當(dāng)在代碼中調(diào)用alloc的時候,例如[GYMPerson alloc],那么在底層,代碼會先去哪里呢?
毫無疑問,當(dāng)?shù)谝淮蝿?chuàng)建GYMPerson對象的時候,是會來到下面這個方法的:

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

其實(shí)這個方法沒什么,那么再來看看callAlloc這個方法

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

在這個方法中有個OBJC2判斷,現(xiàn)在底層的代碼全都是objc2的了,宏定義的值為1.
隨后遇到 fastpath(!cls->ISA()->hasCustomAWZ()) 判斷,因?yàn)檫@個類是第一次創(chuàng)建對象,類還沒有初始化(懶加載),因此無法判斷該類是否實(shí)現(xiàn)了allocWithZone方法,因而判斷也不成立,所以直接跳到下面allocWithZone的判斷,但是callAlloc在調(diào)用的時候,傳入的allocWithZone是false,因此直接走到return,調(diào)用 [cls alloc] 。

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

這一看,這也沒什么啊,別著急,繼續(xù)往下看

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

感覺被欺騙了,怎么又回來了?請注意,參數(shù)值不一樣了,此時的allocWithZone是true了。
此時如果沒有實(shí)現(xiàn)allocWithZone方法,那么 fastpath(!cls->ISA()->hasCustomAWZ()) 判斷則成立,進(jìn)入內(nèi)部的 fastpath(cls->canAllocFast()) 判斷,而這個判斷永遠(yuǎn)是false,如下:

bool canAllocFast() {
     assert(!isFuture());
     return bits.canAllocFast();
}
bool canAllocFast() {
     return false;
}

因此代碼會走到 id obj = class_createInstance(cls, 0) 創(chuàng)建對象。
那么如果用戶實(shí)現(xiàn)了allocWithZone方法,第二次調(diào)用callAlloc傳入的allocWithZone參數(shù)為true,此時 fastpath(!cls->ISA()->hasCustomAWZ()) 判斷不成立,代碼直接走到 [cls allocWithZone:nil] 方法中,然后調(diào)用allocWithZone方法,隨后調(diào)用 _class_createInstanceFromZone 方法。

上面我們提到了一個創(chuàng)建對象的方法class_createInstance(cls, 0),那我們看看這個方法:

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

真是萬變不離其中啊,最終又回到了我們實(shí)現(xiàn)allocWithZone方法后,底層調(diào)用的統(tǒng)一方法 _class_createInstanceFromZone,下面我們來看一下這個方法。

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

    // Read class's info bits all at once for performance
    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) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        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;
}

這個方法中zone為nil, fast經(jīng)判斷后是true,因此代碼會走到下面的代碼中:

if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    }

這個if分支中就做了兩件事情,第一:在內(nèi)存中給這個對象開辟空間,相當(dāng)于在小區(qū)里面申請了一套房子;第二:初始化isa,綁定對應(yīng)的類信息,相當(dāng)于給這套房子弄個房本,里面有具體的信息。
至于calloc和isa以后會講到,還有一個很重要的方法 cls->instanceSize(extraBytes) 馬上就會講到,繼續(xù)往下看哦!

以上則是一個對象的初始化過程,現(xiàn)在我們將上面的簡易圖復(fù)雜化一下:


4. 對象空間大小及內(nèi)存對齊

上面我們主要探究了對象創(chuàng)建的流程,現(xiàn)在我們說一下對象所需要空間的大小,以及字節(jié)對齊問題。
還記得剛才說過的方法嗎?

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

這個方法主要計(jì)算對象所需要的內(nèi)存空間的大小,在了解如何計(jì)算之前,我們先看一下內(nèi)存對齊原則:

  1. 結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第?個數(shù)據(jù)成員放在位置為0的地?,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員??或者成員的?成員??(只要該成員有?成員,?如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(?如int為4字節(jié),則要從4的整數(shù)倍地址開始)存儲。

  2. 如果?個結(jié)構(gòu)?有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最?元素??的整數(shù)倍地址開始存儲.(struct a?存有struct b,b?有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始儲)。

  3. 結(jié)構(gòu)體的總??,必須是其內(nèi)部最?成員的整數(shù)倍.不?的要補(bǔ)齊。
    聽起來是不是有些亂呢,來,還是看代碼理解吧,下面有四個結(jié)構(gòu)體:

struct Struct1 {
    char a;
    int b;
    double c;
    short d;
} Struct1;

struct Struct2 {
    double ;
    int b;
    char c;
    short d;
} Struct2;

struct Struct3 {
    int a;
    double b;
    char c;
    short d;
    struct Struct2 e;
} Struct3;

struct Struct4 {
    int a;
    double b;
    char c;
    short d;
    struct Struct1 e;
} Struct4;

- (void)instanceSizeFunction {
    NSLog(@"Struct1 size = %lu", sizeof(Struct1));
    NSLog(@"Struct2 size = %lu", sizeof(Struct2));
    NSLog(@"Struct3 size = %lu", sizeof(Struct3));
    NSLog(@"Struct4 size = %lu", sizeof(Struct4));
}

我們調(diào)用instanceSizeFunction方法查看一下結(jié)果:

GYMDemo[41131:3568504] Struct1 size = 24
GYMDemo[41131:3568504] Struct2 size = 16
GYMDemo[41131:3568504] Struct3 size = 40
GYMDemo[41131:3568504] Struct4 size = 48

首先我們分析一下原則1,將其簡化成一個公式:min(position, size),如果是存儲第一個元素,那么直接放到0的位置,從第二個元素開始,采用這個公式,position是第二個及以后元素存儲的最小開始位置,size則是元素的大小(比如int為4字節(jié)),公式的原理就是取position是size的最小整數(shù)倍的值作為存儲某一元素的開始位置。
我們看Struct1結(jié)構(gòu)體:

struct Struct1 {
    char a;  // 1 字節(jié)
    int b;   // 4 字節(jié)
    double c;// 8 字節(jié)
    short d; // 2 字節(jié)
} Struct1;

將a存入0位置的時候,只占用了1個字節(jié),此時position指向下一個可存儲的起始位置,也就是1,而下一個元素b是4字節(jié),那么position就往后移動,當(dāng)為4的時候(4為int(4字節(jié))的整數(shù)倍),存入b,則b存在4 5 6 7四個位置,此時position為8,下一個元素c,8個字節(jié),position正好是整數(shù)倍,于是開始存c,則c存在8 9 10 11 12 13 14 15八個位置,此時position為16,正好是元素d(2字節(jié)的整數(shù)倍),則d存在16 17兩個位置,這么一算結(jié)構(gòu)體Struct1一共占用了18個字符,但是別忘了原則3,結(jié)構(gòu)體整體大小是其內(nèi)部最大元素大小的整數(shù)倍,iOS64位下最大的數(shù)據(jù)類型占8字節(jié),所以結(jié)構(gòu)體總大小應(yīng)是8的整數(shù)倍,那么比18大的最小整數(shù)倍即為24,所以Struct1的總大小為24字節(jié)。
如下入所示:



Struct2內(nèi)存對齊如下:

struct Struct2 {
    double a;  // 8 字節(jié)
    int b;     // 4 字節(jié)
    char c;    // 1 字節(jié)
    short d;   // 2 字節(jié)
} Struct2;

Struct3內(nèi)存對齊:

struct Struct3 {
    int a;      // 4 字節(jié)
    double b;   // 8 字節(jié)
    char c;     // 1 字節(jié)
    short d;    // 2 字節(jié) 
    struct Struct2 e; // 16 字節(jié)
} Struct3;

由結(jié)構(gòu)體定義可知,Struct3中有個結(jié)構(gòu)體成員e,這涉及到了內(nèi)存對齊的第二個原則。
其內(nèi)存對齊如下圖:



至于Struct4,感興趣的朋友可以自己算一算。

上面說完了內(nèi)存對齊的原則以及結(jié)構(gòu)體內(nèi)存對齊,下面回過頭看看創(chuàng)建對象時內(nèi)存大小是如何計(jì)算的。在創(chuàng)建對象的過程中,最后在calloc方法之前,調(diào)用了 instanceSize(size_t extraBytes) 方法計(jì)算了對象申請的內(nèi)存空間大小,見下面的方法:

/**
方法中則調(diào)用 **alignedInstanceSize()** 方法進(jìn)行計(jì)算,另外請注意下面還有個if判斷,如果計(jì)算出來的size<16,那么size就為16.
*/
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;
}
/**
方法中將未內(nèi)存對齊的類的屬性的總大小傳入 **word_align()** 方法中進(jìn)行對齊計(jì)算。
*/
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}
// 返回類的ivar中所有屬性的總大小。
uint32_t unalignedInstanceSize() {
    assert(isRealized());
    return data()->ro->instanceSize;
}
// 計(jì)算內(nèi)存對齊后對象需要的空間大小,詳見下面講解:
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

方法解析:

我們知道NSObject對象有一個屬性,那就是isa,是一個指針類型,所占空間大小為8字節(jié),如果創(chuàng)建一個NSObject,我們看看這個方法如何計(jì)算的。
首先看一下宏定義:
#define WORD_MASK 7UL
很顯然在64位下,WORD_MASK為7.
當(dāng)傳入的x為8的時候,那么x + WORD_MASK為15,其二進(jìn)制為:0000 1111
WORD_MASK的二進(jìn)制為:0000 0111, 那么~WORD_MASK的二進(jìn)制為:1111 1000
那么15 & ~7的計(jì)算為:
    0000 1111
&   1111 1000
=   0000 1000
0000 1000的十進(jìn)制結(jié)果為8,那么當(dāng)傳入x值為8的時候,經(jīng)過計(jì)算后得到的結(jié)果為8字節(jié)。

是不是感覺有些巧合,都是8,好,那么假設(shè)傳入x=9,我們在計(jì)算一遍。

當(dāng)傳入的x為9的時候,那么x + WORD_MASK為16,其二進(jìn)制為:0001 0000
那么16 & ~7的計(jì)算為:
    0001 0000
&   1111 1000
=   0001 0000
0001 0000的十進(jìn)制結(jié)果為16,由此可知,類的屬性總空間大小為9,經(jīng)過對齊后需要的空間為16.

由上面的分析可以,對象申請內(nèi)存空間的大小是8字節(jié)對齊計(jì)算的。

經(jīng)過上面這一波計(jì)算,我們得到的內(nèi)存對齊后的數(shù)值就是對象創(chuàng)建的時候,向內(nèi)存申請的空間大小,那么計(jì)算機(jī)真的是按照這個數(shù)值開辟的空間嗎?請看下面章節(jié)。

5. 系統(tǒng)開辟空間大小

計(jì)算機(jī)系統(tǒng)真的是按照對象申請的空間大小來開辟空間嗎?
答案:不是
系統(tǒng)在calloc方法中,對于開辟多大的空間,有自己的算法。在探索calloc底層源碼的時候,有一個很重要的方法,如下:

segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;
    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

還有兩個關(guān)鍵的宏定義:

#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16

方法解析:

假如方法中傳入的size為24(對象經(jīng)過內(nèi)存對齊后申請空間的大?。覀兛匆幌逻@行:
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
將宏替換掉后:
k = (24 + 16 - 1) >> 4;
即:k = 39 >> 4, 二進(jìn)制表示為:0010 0111 >> 4 = 0000 0010 = 2
再k計(jì)算完后,又進(jìn)行了:
slot_bytes = k << SHIFT_NANO_QUANTUM; 
即 slot_bytes = k << 4,二進(jìn)制表示為:0000 0010 << 4 = 0010 0000 = 32
最終得到的slot_bytes為32,也就是系統(tǒng)為這個對象開辟的實(shí)際空間的大小。

由上可知,在callloc底層,系統(tǒng)將傳入的size右移4位,再左移4位,也就是16字節(jié)對齊。

下面舉個例子:
定義一個GYMDeveloper類,繼承GYMPerson,GYMPerson

@interface GYMPerson : NSObject

@end
@interface GYMDeveloper : GYMPerson
// isa // 8字節(jié)
@property (nonatomic, copy) NSString *name; // 8字節(jié)
@property (nonatomic, assign) int age; //4字節(jié)
@property (nonatomic, assign) long height;  // 8字節(jié)
@property (nonatomic, copy) NSString *selfIntroduce; // 8字節(jié)
@end

對于GYMDeveloper,如果要創(chuàng)建一個GYMDeveloper的實(shí)例對象,很容易就會算出該對象所需要的空間大小,即40字節(jié),不要忘了老祖宗NSObject還有isa指針,占8字節(jié)呢。
我們通過下面的方法測試一下:

- (void)instanceSizeFunction {
    GYMDeveloper *developer = [GYMDeveloper alloc];
    NSLog(@"對象申請的空間是:%lu字節(jié), 系統(tǒng)開辟的空間是:%lu字節(jié)", class_getInstanceSize([developer class]), malloc_size((__bridge const void *)(developer)));
}

輸出結(jié)果為:

GYMDemo[51386:4021321] 對象申請的空間是:40字節(jié), 系統(tǒng)開辟的空間是:48字節(jié)

綜上所述:對象申請空間的大小是8字節(jié)對齊計(jì)算的,而系統(tǒng)為對象開辟空間是16字節(jié)對齊計(jì)算的。

寫在最后:寫文章不容易,如果您覺得好就給個贊,如果文章有問題還請指正,轉(zhuǎn)載的話,請標(biāo)注原文地址哦!

原文作者:Daniel_Coder

原文地址:https://blog.csdn.net/guoyongming925/article/details/108859202

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

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

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