問(wèn)題提出:
解析過(guò)程
要知道NSObject對(duì)象占用多少內(nèi)存,需要知道創(chuàng)建一個(gè)NSObject對(duì)象的時(shí)候都做了什么。
比如下面這行代碼:
NSObject *object = [[NSObject alloc] init];
首先我們點(diǎn)進(jìn)去看看NSObject是怎么定義的
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
然后發(fā)現(xiàn)NSObject的聲明中只有一個(gè)class 類(lèi)型的isa指針成員變量,按住command鍵看一下class又是個(gè)什么東西呢。點(diǎn)進(jìn)去我們發(fā)現(xiàn),class是一個(gè)指向結(jié)構(gòu)體的指針,具體是什么這個(gè)結(jié)構(gòu)體是什么我們先不管。
typedef struct objc_class *Class;
另外我們知道平時(shí)編寫(xiě)的Objective-C代碼底層實(shí)現(xiàn)其實(shí)是C/C++代碼,然后轉(zhuǎn)成匯編語(yǔ)言,最后生成機(jī)器閱讀的機(jī)器語(yǔ)言也就是01010110這種。
也就是:

既然這樣,那我們就看看NSObject 轉(zhuǎn)成C/C++代碼是什么樣的。
通過(guò)下面任何一種方式都可以將OC語(yǔ)言轉(zhuǎn)成C/C++
方式一:clang -rewrite-objc main.m(main.m是要被轉(zhuǎn)C/C++的文件名字) -o main.cpp(main.cpp是要生成C/C++的文件名字)
方式二:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m(這里是要被轉(zhuǎn)C/C++的文件名字) -o main-arm64.cpp(main-arm64.cpp是要生成C/C++的文件名字)
方式一是將OC直接轉(zhuǎn)成C/C++ 語(yǔ)言,(注:-o main.cpp 這個(gè)是要生成C/C++的文件名,可寫(xiě)可不寫(xiě))
方式二是將OC轉(zhuǎn)成適應(yīng)于 64位真機(jī)下的 C/C++ 語(yǔ)言,-o main-arm64.cpp這行可寫(xiě)可不寫(xiě),默認(rèn)生成<原類(lèi)名.cpp>文件
在此我們也舉一個(gè)簡(jiǎn)單的例子,直接創(chuàng)建一個(gè)macOS項(xiàng)目就好(等下后面運(yùn)行不需啟動(dòng)模擬器)。
然后在main.m文件的main函數(shù)里簡(jiǎn)單寫(xiě)一行代碼
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obejct = [[NSObject alloc] init];
}
return 0;
}
接著打開(kāi)終端。cd到文件目錄下,然后選擇上面任何一種轉(zhuǎn)成C/C++的方式,這里我用方式二
即:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
然后就會(huì)生成一個(gè) main-arm64.cpp 文件,我們打開(kāi)看一下。
直接搜索NSObject,就會(huì)發(fā)現(xiàn)有這么一個(gè)結(jié)構(gòu)體
struct NSObject_IMPL {
Class isa;
};
此處我們也可以得之,OC的類(lèi)在C/C++中實(shí)現(xiàn)的方式就是一個(gè)結(jié)構(gòu)體,即OC類(lèi)是以結(jié)構(gòu)體的形式在內(nèi)存中存在的。而且NSObject內(nèi)部只有一個(gè)class 類(lèi)型的isa指針,既然是一個(gè)指針,那么我們知道,指針在64環(huán)境下就是占8個(gè)字節(jié)(在32位環(huán)境下就是4個(gè)字節(jié)。)既然這個(gè)指針占用8個(gè)字節(jié),而且NSObject這個(gè)結(jié)構(gòu)體內(nèi)部只有這么一個(gè)結(jié)構(gòu)體,很顯然,這個(gè)結(jié)構(gòu)體也就是占用8個(gè)字節(jié)。
所以,
NSObject *obejct = [[NSObject alloc] init];
這句代碼的過(guò)程就是:
[NSObject alloc]在內(nèi)存中分配地址,然后創(chuàng)建對(duì)象。接著將object這個(gè)指針指向剛剛創(chuàng)建的對(duì)象,最后將分配的對(duì)象地址賦值給object指針。也就是

那么這樣看來(lái),創(chuàng)建一個(gè)NSObject對(duì)象具體分配的大小 就是8個(gè)字節(jié),因?yàn)榍懊嬖蹅円呀?jīng)分析出,NSObject底層實(shí)現(xiàn)方式其實(shí)就是一個(gè)結(jié)構(gòu)體,而這個(gè)結(jié)構(gòu)體內(nèi)部只有一個(gè)占8個(gè)字節(jié)的isa指針。到底是不是呢需要驗(yàn)證一下。
runtime里面有這么一個(gè)函數(shù),獲取類(lèi)的實(shí)例大小
class_getInstanceSize(Class _Nullable cls)
既然有這么個(gè)方法,那咱們就打印驗(yàn)證一下,看是不是8個(gè)字節(jié)呢,
注意要先引入
#import <objc/runtime.h>
NSLog(@"NSObject InstanceSize:%zd",class_getInstanceSize([NSObject class]));
結(jié)果:NSObject InstanceSize:8
經(jīng)過(guò)測(cè)試發(fā)現(xiàn),“果然”是8個(gè)字節(jié)。有眼尖的小伙伴可能會(huì)發(fā)現(xiàn)這里可能會(huì)有坑。為了確保安全,我們需要看看 class_getInstanceSize(Class _Nullable cls)
這里是OC源碼地址
https://opensource.apple.com/tarballs/
打開(kāi)鏈接往下翻或直接搜索 objc4,點(diǎn)進(jìn)去會(huì)發(fā)現(xiàn)好多版本,然后找到最大的那個(gè),也就是最新的下載、解壓。打開(kāi)解壓好的源碼,這是不能直接運(yùn)行的,但是我們可以大略看一下。
打開(kāi)之后直接搜索 class_getInstanceSize,找.mm文件看實(shí)現(xiàn)代碼

我們發(fā)現(xiàn),在調(diào)用class_getInstanceSize(Class _Nullable cls)函數(shù)的時(shí)候,如果傳入的類(lèi)對(duì)象為nil,就會(huì)直接返回0。否則就調(diào)用alignedInstanceSize函數(shù),也就是對(duì)齊后的實(shí)例大小,接著點(diǎn)擊去發(fā)現(xiàn)

調(diào)這個(gè)方法的意思是:返回unalignedInstanceSize(未對(duì)齊的實(shí)例大小)對(duì)齊后的大小
這也是遵循了內(nèi)存分配法則之一:內(nèi)存對(duì)齊原則。但這個(gè)函數(shù)的注釋是返回class的成員變量大小,注意,此處可有有蹊蹺了。既然這個(gè)函數(shù)式返回對(duì)象的成員變量的大小,那么這個(gè)大小是不是就是創(chuàng)建對(duì)象時(shí)分配的大小呢?為了證明我們也看看
NSObject的alloc方法是怎么實(shí)現(xiàn)的。對(duì)象調(diào)用alloc方法其實(shí)就是調(diào)用了allocwithzone,我們直接在剛剛下載的OC源碼里搜索allocwithzone,找到這個(gè)函數(shù)的實(shí)現(xiàn),發(fā)現(xiàn)在這個(gè)函數(shù)里調(diào)用了一個(gè)創(chuàng)建實(shí)例對(duì)象的函數(shù)
接著我們看看這個(gè)函數(shù)式怎么實(shí)現(xiàn)的,點(diǎn)進(jìn)去發(fā)現(xiàn)這里面有調(diào)用了一個(gè)獲取實(shí)例對(duì)象大小的函數(shù)

然后我們?cè)龠M(jìn)一步點(diǎn)進(jìn)去發(fā)現(xiàn)里面計(jì)算的size是調(diào)用了instanceSize(size_t)這個(gè)函數(shù)

我們接著點(diǎn)進(jìn)去,最后找到這個(gè),也就是實(shí)例對(duì)象的大小

我們發(fā)現(xiàn)這個(gè)函數(shù)的有一行注釋?zhuān)馑季褪荂oreFoundation框架規(guī)定所有的對(duì)象至少要16個(gè)字節(jié)。也就是如果這個(gè)對(duì)象不足16個(gè)字節(jié),也會(huì)分配給你16個(gè)字節(jié)。
雖然這么說(shuō),我們也得驗(yàn)證一下
alloc在創(chuàng)建的時(shí)候是不是真的分配了16個(gè)字節(jié)。首先我們?cè)谥白约簞?chuàng)建的文件里導(dǎo)入
#import <malloc/malloc.h>
然后會(huì)查看這個(gè)函數(shù)
extern size_t malloc_size(const void *ptr);
/* Returns size of given ptr */
malloc的全稱(chēng)是memory allocation,中文叫動(dòng)態(tài)內(nèi)存分配,用于申請(qǐng)一塊連續(xù)的指定大小的內(nèi)存塊區(qū)域以void*類(lèi)型返回分配的內(nèi)存區(qū)域地址。extern size_t malloc_size(const void *ptr) 函數(shù)的意思就是返回一個(gè)指針的占用內(nèi)存的大小,既然這樣我們就把創(chuàng)建的object傳進(jìn)去,看看大小到底是多少,
NSLog(@"obejct: %zd",malloc_size((__bridge const void *)(obejct)));
結(jié)果:obejct malloc_size: 16
打印結(jié)果的確是16個(gè)字節(jié)。
根據(jù)打印結(jié)果結(jié)合源碼解析我們就可以得知?jiǎng)?chuàng)建NSObject對(duì)象的時(shí)候系統(tǒng)會(huì)分配給其16個(gè)字節(jié),但是這個(gè)對(duì)象只使用了8個(gè)字節(jié),另外8個(gè)字節(jié)是空著的。
而且咱們也可以根據(jù)對(duì)象地址查看內(nèi)存空間地址,首先在創(chuàng)建NSObject的時(shí)候添加個(gè)斷點(diǎn),打印一下obejct對(duì)象的地址:0x10062f930,然后找Debug->Debug Workflow -> View Memory,接著將地址輸入,就可以查看。


根據(jù)內(nèi)存空間地址,我們也可以發(fā)現(xiàn)。isa占用了前面8個(gè)字節(jié)(因?yàn)椴榭吹氖?6進(jìn)制,一位16進(jìn)制代表4位二進(jìn)制,所以?xún)晌?6進(jìn)制就是8位二進(jìn)制也就是一個(gè)字節(jié)),而后面八個(gè)字節(jié)都是00,再后面就是其他內(nèi)存了。
簡(jiǎn)單畫(huà)張圖表也就是頁(yè)面這個(gè)樣子

雖然弄明白了NSObject,但是開(kāi)發(fā)最多的還是用自定義的類(lèi)。下面就看一下自定義的類(lèi)所占的內(nèi)存空間大小。
在此為了方便查看,我就直接在main文件直接創(chuàng)建一個(gè)SSPerson類(lèi),類(lèi)里面有兩個(gè)成員變量
@interface SSPerson : NSObject{
@public
int _age;
int _height;
}
@end
@implementation SSPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
然后我們?cè)俅蝝ain.m文件轉(zhuǎn)為C/C++文件,看看自定義的SSPerson類(lèi)轉(zhuǎn)成C/C++是什么格式的,方法和前面一樣。
然后搜索SSPerson就會(huì)發(fā)現(xiàn),
struct SSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
我們發(fā)現(xiàn)自定的SSPerson類(lèi)也是轉(zhuǎn)成了一個(gè)結(jié)構(gòu)體,內(nèi)部包含三個(gè)成員變量,下面兩個(gè)是咱們自己寫(xiě)的,NSObject_IVARS 是一個(gè)struct NSObject_IMPL結(jié)構(gòu)體類(lèi)型,前面我們說(shuō)過(guò),NSObject底層實(shí)現(xiàn)就是struct NSObject_IMPL,而其內(nèi)部就只有一個(gè)class類(lèi)型的 isa 成員,所以這里可以直接當(dāng)做是Class isa,
即
struct SSPerson_IMPL {
Class isa;
int _age;
int _height;
};
那么根據(jù)之前分析(后面如果沒(méi)有特別說(shuō)明都是結(jié)合64位環(huán)境下計(jì)算),isa占8個(gè)字節(jié),而且int類(lèi)型占用4個(gè)字節(jié),這樣算來(lái),就是
一個(gè)isa 8個(gè)字節(jié) + int類(lèi)型 4個(gè)字節(jié) + int類(lèi)型 4個(gè)字節(jié) = 16個(gè)字節(jié)。
我們也可以進(jìn)一步查看SSPerson對(duì)象的內(nèi)存空間地

第一個(gè)是isa占用8個(gè)字節(jié),接著是_age占用4個(gè)字節(jié),然后是_height占用4個(gè)字節(jié)。
那么SSPerson是不是占16個(gè)字節(jié),我們打印一下便知。
NSLog(@"SSPerson InstanceSize:%zd",class_getInstanceSize([SSPerson class]));
NSLog(@"person malloc_size: %zd",malloc_size((__bridge const void *)(person)));
結(jié)果為:
SSPerson InstanceSize:16
person malloc_size: 16
由此可我們也可以得知,創(chuàng)建SSPerson對(duì)象的時(shí)候系統(tǒng)會(huì)分配給其16個(gè)字節(jié),而且其使用了16個(gè)字節(jié)。
如果感興趣,可以進(jìn)一步嘗試,多嘗試就會(huì)有多發(fā)現(xiàn)。
答案:
*系統(tǒng)分配了16個(gè)字節(jié)給NSObject對(duì)象(通過(guò)malloc_size函數(shù)獲得)
*但NSObject對(duì)象內(nèi)部只使用了8個(gè)字節(jié)的空間(64bit環(huán)境下,可以通過(guò)class_getInstanceSize函數(shù)獲得)