OC對象的本質(zhì)(上):OC對象的底層實(shí)現(xiàn)原理

一個(gè)NSObject對象占用多少內(nèi)存?

Objective-C的本質(zhì)
平時(shí)我們編寫的OC代碼,底層實(shí)現(xiàn)都是C/C++代碼

Objective-C --> C/C++ --> 匯編語言 --> 機(jī)器碼

所以O(shè)bjective-C的面向?qū)ο蠖际腔贑/C++的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的,所以我們可以將Objective-C代碼轉(zhuǎn)換成C/C++代碼,來研究OC對象的本質(zhì)。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

我們在main函數(shù)里面定義一個(gè)簡單對象,然后通過 clang -rewrite-objc main.m -o main.cpp命令,將main.m文件進(jìn)行重寫,即可轉(zhuǎn)換出對應(yīng)的C/C++代碼。但是可以看到一個(gè)問題,就是轉(zhuǎn)換出來的文件過長,將近10w行。

截屏---下午2.51.00.png

因?yàn)椴煌脚_支持的代碼不同(Windows/Mac/iOS),那么同樣一句OC代碼,經(jīng)過編譯,轉(zhuǎn)成C/C++代碼,以及最終的匯編碼,是不一樣的,匯編指令嚴(yán)重依賴平臺環(huán)境。
我們當(dāng)前關(guān)注iOS開發(fā),所以,我們只需要生成iOS支持的C/C++代碼。因此,可以使用如下命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <輸出的cpp文件>
-sdk:指定sdk
-arch:指定機(jī)器cpu架構(gòu)(模擬器-i386、32bit、64bit-arm64 )
如果需要鏈接其他框架,使用-framework參數(shù),比如-framework UIKit
一般我們手機(jī)都已經(jīng)普及arm64,所以這里的架構(gòu)參數(shù)用arm64,生成的cpp代碼如下


截屏---下午2.51.23.png

截屏--- 下午2.52.42.png

接下來,我們查看一下main_arm64.cpp源文件,如果熟悉這個(gè)文件,你將會發(fā)現(xiàn)這么一個(gè)結(jié)構(gòu)體

struct NSObject_IMPL {
    Class isa;
};

我們再來對比看一下NSObject頭文件的定義

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end

簡化一下,就是

@interface NSObject  {
    Class isa ;
}
@end

是不是猜到點(diǎn)什么了?沒錯(cuò),struct NSObject_IMPL其實(shí)就是NSObject的底層結(jié)構(gòu),或者說底層實(shí)現(xiàn)。換個(gè)角度理解,可以說C/C++的結(jié)構(gòu)體類型支撐了OC的面相對象。

點(diǎn)進(jìn)Class的定義,我們可以看到 是typedef struct objc_class *Class;

Class isa; 等價(jià)于 struct objc_class *isa;

所以NSObject對象內(nèi)部就是放了一個(gè)名叫isa的指針,指向了一個(gè)結(jié)構(gòu)體 struct objc_class。

總結(jié)一:一個(gè)OC對象在內(nèi)存中是如何布局的?

截屏--- 下午3.09.04.png

猜想:NSObject對象的底層就是一個(gè)包含了一個(gè)指針的結(jié)構(gòu)體,那么它的大小是不是就是8字節(jié)(64位下指針類型占8個(gè)字節(jié))?
為了驗(yàn)證猜想,我們需要借助runtime提供的一些工具,導(dǎo)入runtime頭文件,class_getInstanceSize ()方法可以計(jì)算一個(gè)類的實(shí)例對象所實(shí)際需要的的空間大小

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        size_t size = class_getInstanceSize([NSObject class]);
        NSLog(@"NSObject對象的大?。?zd",size);
    }
    return 0;
}

結(jié)果是

等等,就這么簡單?確定嗎?答案是否定的~~~
介紹另一個(gè)庫#import <malloc/malloc.h>,其下有個(gè)方法 malloc_size(),該函數(shù)的參數(shù)是一個(gè)指針,可以計(jì)算所傳入指針 所指向內(nèi)存空間的大小。我們來用一下

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        size_t size = class_getInstanceSize([NSObject class]);
        NSLog(@"NSObject實(shí)例對象的大?。?zd",size);
        size_t size2 = malloc_size((__bridge const void *)(obj));
        NSLog(@"對象obj所指向的的內(nèi)存空間大?。?zd",size2);
    }
    return 0;
}

結(jié)果是16,如何解釋呢?

15:10:12.487428+0800 Interview01-OC對象的本質(zhì)[2881:150600] 8
15:10:12.487939+0800 Interview01-OC對象的本質(zhì)[2881:150600] 16
Program ended with exit code: 0

想要真正弄清楚其中的緣由,就需要去蘋果官方的開源代碼里面去一探究竟了。蘋果的開源代請看這里。
先看一下class_getInstanceSize的實(shí)現(xiàn)。我們需要進(jìn)到objc4/文件里面下載一份最新的源碼,我當(dāng)前最新的版本是objc4-750.1.tar.gz。下載解壓之后,打開工程,就可以查看runtime的實(shí)現(xiàn)源碼。
搜索class_getInstanceSize找到實(shí)現(xiàn)代碼

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

再點(diǎn)進(jìn)alignedInstanceSize方法的實(shí)現(xiàn)

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

可以看到該方法的注釋說明Class's ivar size rounded up to a pointer-size boundary.,意思就是獲得類的成員變量的大小,其實(shí)也就是計(jì)算類所對應(yīng)的底層結(jié)構(gòu)體的大小,注意后面的這個(gè)rounded up to a pointer-size boundary指的是系統(tǒng)在為類的結(jié)構(gòu)體分配內(nèi)存時(shí)所進(jìn)行的內(nèi)存對齊,要以一個(gè)指針的長度作為對齊系數(shù),64位系統(tǒng)指針長度(字長)是8個(gè)字節(jié),那么返回的結(jié)果肯定是8的最小整數(shù)倍。為什么需要用指針長度作為對齊系數(shù)呢?因?yàn)轭愃鶎?yīng)的結(jié)構(gòu)體,在頭部的肯定是一個(gè)isa指針,所以指針肯定是該結(jié)構(gòu)體中最大的基本數(shù)據(jù)類型,所以根據(jù)結(jié)構(gòu)體的內(nèi)存對齊規(guī)則,才做此設(shè)定。如果對這里有疑惑的話,請先復(fù)習(xí)一下有關(guān)內(nèi)存對齊的知識,便一目了然了。
所以class_getInstanceSize方法,可以幫我們獲取一個(gè)類的的實(shí)例對象所對應(yīng)的結(jié)構(gòu)體的實(shí)際大小。

我們再從alloc方法探究一下,alloc方法里面實(shí)際上是AllocWithZone方法,我們在objc源碼工程里面搜索一下,可以在Object.mm文件里面找到一個(gè)_objc_rootAllocWithZone方法。

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}

再點(diǎn)進(jìn)里面的關(guān)鍵方法class_createInstance的實(shí)現(xiàn)看一下

id  class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}
繼續(xù)點(diǎn)進(jìn)_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;
}

這個(gè)方法有點(diǎn)長,有時(shí)分析一個(gè)方法,不要過分拘泥細(xì)節(jié),先針對我們尋找的問題,找到關(guān)鍵點(diǎn),像這個(gè)比較長的方法,我們知道,它的主要功能就是創(chuàng)建一個(gè)實(shí)例,為其開辟內(nèi)存空間,我們可以發(fā)現(xiàn)中間的這句代碼obj = (id)calloc(1, size);,是在分配內(nèi)存,這里的size是需要分配的內(nèi)存的大小,那這句應(yīng)該就是為對象開辟內(nèi)存的核心代碼,再看它里面的參數(shù)size,我們能在上兩行代碼中找到size_t size = cls->instanceSize(extraBytes);,于是我們繼續(xù)點(diǎn)進(jìn)instanceSize看看

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

翻譯一下這句注//CF requires all objects be at least 16 bytes.我們就明白了,CF作出了硬性的規(guī)定:當(dāng)創(chuàng)建一個(gè)實(shí)例對象的時(shí)候,為其分配的空間不能小于16個(gè)字節(jié),為什么這么規(guī)定呢,我個(gè)人目前的理解是這可能就相當(dāng)于一種開發(fā)規(guī)范,或者對于CF框架內(nèi)部的一些實(shí)現(xiàn)提供的規(guī)范。
這個(gè)size_t instanceSize(size_t extraBytes)返回的字節(jié)數(shù),其實(shí)就是為 為一個(gè)類創(chuàng)建實(shí)例對象所需要分配的內(nèi)存空間。這里我們的NSObject類創(chuàng)建一個(gè)實(shí)例對象,就分配了16個(gè)字節(jié)。
我們在點(diǎn)進(jìn)上面代碼中的alignedInstanceSize方法

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

這不就是我們上面分析class_getInstanceSize方法里面看到的那個(gè)alignedInstanceSize嘛。

截屏2021-06-25 下午3.11.28.png

總結(jié)二:class_getInstanceSize&malloc_size的區(qū)別
class_getInstanceSize:獲取一個(gè)objc類的實(shí)例的實(shí)際大小,這個(gè)大小可以理解為創(chuàng)建這個(gè)實(shí)例對象至少需要的空間(系統(tǒng)實(shí)際為這個(gè)對象分配的空間可能會比這個(gè)大,這是出于系統(tǒng)內(nèi)存對齊的原因)。
malloc_size:得到一個(gè)指針?biāo)赶虻膬?nèi)存空間的大小。我們的OC對象就是一個(gè)指針,利用這個(gè)函數(shù),我們可以得到該對象所占用的內(nèi)存大小,也就是系統(tǒng)為這個(gè)對象(指針)所指向?qū)ο笏鶎?shí)際分配的內(nèi)存大小。
sizeof():獲取一個(gè)類型或者變量所占用的存儲空間,這是一個(gè)運(yùn)算符。
[NSObject alloc]之后,系統(tǒng)為其分配了16個(gè)字節(jié)的內(nèi)存,最終obj對象(也就是struct NSObject_IMPL結(jié)構(gòu)體),實(shí)際使用了其中的8個(gè)字節(jié)內(nèi)存,(也就是其內(nèi)部的那個(gè)isa指針?biāo)玫?個(gè)字節(jié),這里我們是在64位系統(tǒng)為前提下來說的)
關(guān)于運(yùn)算符和函數(shù)的一些對比理解

函數(shù)在編譯完之后,是可以在程序運(yùn)行階段被調(diào)用的,有調(diào)用行為的發(fā)生
運(yùn)算符則是在編譯按一刻,直接被替換成運(yùn)算后的結(jié)果常量,跟宏定義有些類似,不存在調(diào)用的行為,所以效率非常高
更為復(fù)雜的自定義類
我們開發(fā)中會自定義各種各樣的類,基本上都是NSObject的子類。更為復(fù)雜的子類對象的內(nèi)存布局又是如何的呢?我們新建一個(gè)NSObject的子類Student,并為其增加一些成員變量

@interface Student : NSObject
{
   @public
    int _age;
    int _no;
}

@end

@implementation Student

@end

使用我們之前介紹過的方法,查看一下這個(gè)類的底層實(shí)現(xiàn)代碼

struct NSObject_IMPL {
    Class isa;
};

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _no;  
    
};

我們發(fā)現(xiàn)其實(shí)Student的底層結(jié)構(gòu)里,包含了它的成員變量,還有一個(gè)NSObject_IMPL結(jié)構(gòu)體變量,也就是它的父類的結(jié)構(gòu)體。根據(jù)我們上面的總結(jié),NSObject_IMPL結(jié)構(gòu)體需要的空間是8字節(jié),但是系統(tǒng)給NSObject對象實(shí)際分配的內(nèi)存是16字節(jié),那么這里Student的底層結(jié)構(gòu)體里面的成員變量NSObject_IMPL應(yīng)該會得到多少的內(nèi)存分配呢?我們驗(yàn)證一下。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        //獲取`NSObject`類的實(shí)例對象的成員變量所占用的大小
        size_t size = class_getInstanceSize([NSObject class]);
        NSLog(@"NSObject實(shí)例對象的大?。?zd",size);
        //獲取obj所指向的內(nèi)存空間的大小
        size_t size2 = malloc_size((__bridge const void *)(obj));
        NSLog(@"對象obj所指向的的內(nèi)存空間大?。?zd",size2);
        
        Student * std = [[Student alloc]init];
        size_t size3 = class_getInstanceSize([Student class]);
        NSLog(@"Student實(shí)例對象的大小:%zd",size3);
        size_t size4 = malloc_size((__bridge const void *)(std));
        NSLog(@"對象std所指向的的內(nèi)存空間大?。?zd",size4);
    }
    return 0;
}

從結(jié)果可以看出,Student類的底層結(jié)構(gòu)體等同于

struct Student_IMPL {
    Class isa;
    int _age;
    int _no;      
};

總結(jié)一下就是,一個(gè)子類的底層結(jié)構(gòu)體,相當(dāng)于 其父類結(jié)構(gòu)體里面的所有成員變量 + 該子類自身定義的成員變量 所組成的一個(gè)結(jié)構(gòu)體。
出于嚴(yán)謹(jǐn),我又給Student類多加了幾個(gè)成員變量,驗(yàn)證我的猜想。

@interface Student : NSObject
{
   @public
    int _age;
    int _no;
    int _grade;
}

貌似是對的了,但是為什么用malloc_size得到std所被分配的內(nèi)存是32?再來一發(fā)試試

@interface Student : NSObject
{

   @public
    //父類的isa還會占用8個(gè)字節(jié)
    int _age;//4字節(jié)
    int _no;//4字節(jié)
    int _grade;//4字節(jié)
    int *p1;//8字節(jié)
    int *p2;//8字節(jié) 
}

Student結(jié)構(gòu)體所有成員變量所需要的總空間為 36字節(jié),根據(jù)內(nèi)存對齊原則,最后結(jié)構(gòu)體所需要的空間應(yīng)該是8的倍數(shù),那應(yīng)該就是40,我們看一下結(jié)果

從結(jié)果看沒錯(cuò),但是同時(shí)也發(fā)現(xiàn)了一個(gè)規(guī)律,隨著std對象成員變量的增加,系統(tǒng)為Student對象std分配的內(nèi)存空間總是以16的倍數(shù)增加(16~32~48…),我們之前分析源碼好像沒看到有做這個(gè)設(shè)定

其實(shí)上面這個(gè)方法只是可以用來計(jì)算一個(gè)結(jié)構(gòu)體對象所實(shí)際需要的內(nèi)存大小。 [update]其實(shí)instanceSize()–>alignedInstanceSize()只是可以用來計(jì)算一個(gè)結(jié)構(gòu)體對象理論上(按照內(nèi)存對其規(guī)則)所需要分配的內(nèi)存大小。

真正給實(shí)例對象完成分配內(nèi)存操作的是下面這個(gè)方法calloc()

這個(gè)方法位于蘋果源碼的libmalloc文件夾中。但是里面的代碼再往下深究,介于我目前的知識儲備以及專業(yè)出身(數(shù)學(xué)專業(yè)),還是困難比較大。好在從一些大神那里得到了指點(diǎn)。
剛才文章開始,我們討論到了結(jié)構(gòu)體的內(nèi)存對齊,這是針對數(shù)據(jù)結(jié)構(gòu)而言的。從系統(tǒng)層面來說,就以蘋果系統(tǒng)而言,出于對內(nèi)存管理和訪問效率最優(yōu)化的需要,會實(shí)現(xiàn)在內(nèi)存中規(guī)劃出很多塊,這些塊有大有小,但都是16的倍數(shù),比如有的是32,有的是48,在libmalloc源碼的nano_zone.h里面有這么一段代碼

#define NANO_MAX_SIZE    256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

NANO是源碼庫里面的其中一種內(nèi)存分配方法,類似的還有frozen、legacy、magazine、purgeable。

這些是蘋果基于各種場景優(yōu)化需求而設(shè)定的對應(yīng)的內(nèi)存管理相關(guān)的庫,暫時(shí)不用對其過分解讀。
上面的NANO_MAX_SIZE解釋中有個(gè)詞Buckets sized,就是蘋果事先規(guī)劃好的內(nèi)存塊的大小要求,針對nano,內(nèi)存塊都被設(shè)定成16的倍數(shù),并且最大值是256。舉個(gè)例子,如果一個(gè)對象結(jié)構(gòu)體需要46個(gè)字節(jié),那么系統(tǒng)會找一塊48字節(jié)的內(nèi)存塊分配給它用,如果另一個(gè)結(jié)構(gòu)體需要58個(gè)字節(jié),那么系統(tǒng)會找一塊64字節(jié)的內(nèi)存塊分配給它用。
到這里,應(yīng)該就可以基本上解釋清楚,為什么剛才student結(jié)構(gòu)需要40個(gè)字節(jié)的時(shí)候,被分配到的內(nèi)存大小確實(shí)48個(gè)字節(jié)。至此,針對一個(gè)NSObject對象占用內(nèi)存的問題,以及延伸出來的內(nèi)存布局,以及其子類的占內(nèi)存問題,應(yīng)該就都可以得到解答了。

面試題解答

一個(gè)NSObject對象占用多少內(nèi)存?
1)系統(tǒng)分配了16字節(jié)給NSObject對象(通過malloc_size函數(shù)可以獲得)
2)NSObject對象內(nèi)部只使用了8個(gè)字節(jié)的空間,用來存放isa指針變量(64位系統(tǒng)下,可以通過class_getInstanceSize函數(shù)獲得)

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

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

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