OC對象的本質(zhì)(上):OC對象的底層實現(xiàn)原理
OC對象的本質(zhì)(中):OC對象的種類
OC對象的本質(zhì)(下):詳解isa&superclass指針
一個NSObject對象占用多少內(nèi)存?
Objective-C的本質(zhì)
平時我們編寫的OC代碼绣硝,底層實現(xiàn)都是C/C++代碼
Objective-C --> C/C++ --> 匯編語言 --> 機器碼
所以O(shè)bjective-C的面向?qū)ο蠖际腔贑/C++的數(shù)據(jù)結(jié)構(gòu)實現(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ù)里面定義一個簡單對象刹碾,然后通過 clang -rewrite-objc main.m -o main.cpp
命令啼染,將main.m
文件進行重寫,即可轉(zhuǎn)換出對應(yīng)的C/C++代碼投剥。但是可以看到一個問題师脂,就是轉(zhuǎn)換出來的文件過長,將近10w行江锨。
因為不同平臺支持的代碼不同(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
:指定機器cpu架構(gòu)(模擬器-i386铝阐、32bit址貌、64bit-arm64 )
如果需要鏈接其他框架,使用-framework參數(shù)徘键,比如-framework UIKit
一般我們手機都已經(jīng)普及arm64芳誓,所以這里的架構(gòu)參數(shù)用arm64,生成的cpp代碼如下
接下來啊鸭,我們查看一下main_arm64.cpp源文件锹淌,如果熟悉這個文件,你將會發(fā)現(xiàn)這么一個結(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
是不是猜到點什么了赂摆?沒錯,struct NSObject_IMPL
其實就是NSObject的底層結(jié)構(gòu)钟些,或者說底層實現(xiàn)烟号。換個角度理解,可以說C/C++的結(jié)構(gòu)體類型支撐了OC的面相對象政恍。
點進Class的定義汪拥,我們可以看到 是typedef struct objc_class *Class;
Class isa; 等價于 struct objc_class *isa;
所以NSObject對象內(nèi)部就是放了一個名叫isa
的指針,指向了一個結(jié)構(gòu)體 struct objc_class
篙耗。
總結(jié)一:一個OC對象在內(nèi)存中是如何布局的迫筑?
猜想:NSObject對象的底層就是一個包含了一個指針的結(jié)構(gòu)體宪赶,那么它的大小是不是就是8字節(jié)(64位下指針類型占8個字節(jié))?
為了驗證猜想脯燃,我們需要借助runtime提供的一些工具搂妻,導(dǎo)入runtime頭文件,
class_getInstanceSize ()
方法可以計算一個類的實例對象所實際需要的的空間大小
#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é)果是
完美驗證辕棚,it's over欲主,let's go home!
等等逝嚎,就這么簡單扁瓢?確定嗎?答案是否定的~~~
介紹另一個庫#import <malloc/malloc.h>
补君,其下有個方法 malloc_size()
涤妒,該函數(shù)的參數(shù)是一個指針,可以計算所傳入指針 所指向內(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實例對象的大兴稀:%zd",size);
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"對象obj所指向的的內(nèi)存空間大小:%zd",size2);
}
return 0;
}
結(jié)果是16屿储,如何解釋呢贿讹?想要真正弄清楚其中的緣由,就需要去蘋果官方的開源代碼里面去一探究竟了够掠。蘋果的開源代請看這里民褂。
先看一下class_getInstanceSize
的實現(xiàn)。我們需要進到objc4/文件里面下載一份最新的源碼疯潭,我當(dāng)前最新的版本是objc4-750.1.tar.gz赊堪。下載解壓之后,打開工程竖哩,就可以查看runtime的實現(xiàn)源碼哭廉。
搜索class_getInstanceSize
找到實現(xiàn)代碼
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
再點進alignedInstanceSize
方法的實現(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.
,意思就是獲得類的成員變量的大小相叁,其實也就是計算類所對應(yīng)的底層結(jié)構(gòu)體的大小遵绰,注意后面的這個rounded up to a pointer-size boundary
指的是系統(tǒng)在為類的結(jié)構(gòu)體分配內(nèi)存時所進行的內(nèi)存對齊,要以一個指針的長度作為對齊系數(shù)增淹,64位系統(tǒng)指針長度(字長)是8個字節(jié)椿访,那么返回的結(jié)果肯定是8的最小整數(shù)倍。為什么需要用指針長度作為對齊系數(shù)呢虑润?因為類所對應(yīng)的結(jié)構(gòu)體成玫,在頭部的肯定是一個isa
指針,所以指針肯定是該結(jié)構(gòu)體中最大的基本數(shù)據(jù)類型,所以根據(jù)結(jié)構(gòu)體的內(nèi)存對齊規(guī)則哭当,才做此設(shè)定猪腕。如果對這里有疑惑的話,請先復(fù)習(xí)一下有關(guān)內(nèi)存對齊的知識荣病,便一目了然了。
所以class_getInstanceSize
方法渗柿,可以幫我們獲取一個類的的實例對象所對應(yīng)的結(jié)構(gòu)體的實際大小个盆。
我們再從alloc
方法探究一下,alloc
方法里面實際上是AllocWithZone
方法朵栖,我們在objc
源碼工程里面搜索一下颊亮,可以在Object.mm
文件里面找到一個_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;
}
再點進里面的關(guān)鍵方法class_createInstance
的實現(xiàn)看一下
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
繼續(xù)點進_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;
}
這個方法有點長陨溅,有時分析一個方法终惑,不要過分拘泥細(xì)節(jié),先針對我們尋找的問題门扇,找到關(guān)鍵點雹有,像這個比較長的方法,我們知道臼寄,它的主要功能就是創(chuàng)建一個實例霸奕,為其開辟內(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ù)點進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)建一個實例對象的時候魄揉,為其分配的空間不能小于16
個字節(jié),為什么這么規(guī)定呢拭宁,我個人目前的理解是這可能就相當(dāng)于一種開發(fā)規(guī)范什猖,或者對于CF框架內(nèi)部的一些實現(xiàn)提供的規(guī)范。
這個size_t instanceSize(size_t extraBytes)
返回的字節(jié)數(shù)红淡,其實就是為 為一個類創(chuàng)建實例對象所需要分配的內(nèi)存空間不狮。這里我們的NSObject
類創(chuàng)建一個實例對象,就分配了16個字節(jié)在旱。
我們在點進上面代碼中的alignedInstanceSize
方法
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
這不就是我們上面分析class_getInstanceSize
方法里面看到的那個alignedInstanceSize
嘛摇零。
總結(jié)二:class_getInstanceSize
&malloc_size
的區(qū)別
-
class_getInstanceSize
:獲取一個objc類的實例的實際大小,這個大小可以理解為創(chuàng)建這個實例對象至少需要的空間(系統(tǒng)實際為這個對象分配的空間可能會比這個大桶蝎,這是出于系統(tǒng)內(nèi)存對齊的原因)驻仅。 -
malloc_size
:得到一個指針?biāo)赶虻膬?nèi)存空間的大小谅畅。我們的OC對象就是一個指針,利用這個函數(shù)噪服,我們可以得到該對象所占用的內(nèi)存大小毡泻,也就是系統(tǒng)為這個對象(指針)所指向?qū)ο笏鶎嶋H分配的內(nèi)存大小。
sizeof()
:獲取一個類型或者變量所占用的存儲空間粘优,這是一個運算符仇味。 -
[NSObject alloc]
之后,系統(tǒng)為其分配了16個字節(jié)的內(nèi)存雹顺,最終obj
對象(也就是struct NSObject_IMPL
結(jié)構(gòu)體)丹墨,實際使用了其中的8個字節(jié)內(nèi)存,(也就是其內(nèi)部的那個isa
指針?biāo)玫?個字節(jié)嬉愧,這里我們是在64位系統(tǒng)為前提下來說的)
關(guān)于運算符和函數(shù)的一些對比理解
- 函數(shù)在編譯完之后贩挣,是可以在程序運行階段被調(diào)用的,有調(diào)用行為的發(fā)生
- 運算符則是在編譯按一刻没酣,直接被替換成運算后的結(jié)果常量王财,跟宏定義有些類似,不存在調(diào)用的行為裕便,所以效率非常高
更為復(fù)雜的自定義類
我們開發(fā)中會自定義各種各樣的類搪搏,基本上都是NSObject
的子類。更為復(fù)雜的子類對象的內(nèi)存布局又是如何的呢闪金?我們新建一個NSObject
的子類Student
疯溺,并為其增加一些成員變量
@interface Student : NSObject
{
@public
int _age;
int _no;
}
@end
@implementation Student
@end
使用我們之前介紹過的方法,查看一下這個類的底層實現(xiàn)代碼
struct NSObject_IMPL {
Class isa;
};
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;
};
我們發(fā)現(xiàn)其實Student
的底層結(jié)構(gòu)里哎垦,包含了它的成員變量囱嫩,還有一個NSObject_IMPL
結(jié)構(gòu)體變量,也就是它的父類的結(jié)構(gòu)體漏设。根據(jù)我們上面的總結(jié)墨闲,NSObject_IMPL
結(jié)構(gòu)體需要的空間是8字節(jié),但是系統(tǒng)給NSObject
對象實際分配的內(nèi)存是16字節(jié)郑口,那么這里Student
的底層結(jié)構(gòu)體里面的成員變量NSObject_IMPL
應(yīng)該會得到多少的內(nèi)存分配呢鸳碧?我們驗證一下。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
//獲取`NSObject`類的實例對象的成員變量所占用的大小
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject實例對象的大腥浴:%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實例對象的大小:%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é)一下就是,一個子類的底層結(jié)構(gòu)體,相當(dāng)于 其父類結(jié)構(gòu)體里面的所有成員變量 + 該子類自身定義的成員變量 所組成的一個結(jié)構(gòu)體肉迫。
出于嚴(yán)謹(jǐn)验辞,我又給Student類多加了幾個成員變量,驗證我的猜想喊衫。
@interface Student : NSObject
{
@public
int _age;
int _no;
int _grade;
}
貌似是對的了跌造,但是為什么用malloc_size
得到std
所被分配的內(nèi)存是32?再來一發(fā)試試
@interface Student : NSObject
{
@public
//父類的isa還會占用8個字節(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é)果看沒錯撑碴,但是同時也發(fā)現(xiàn)了一個規(guī)律撑教,隨著
std
對象成員變量的增加朝墩,系統(tǒng)為Student
對象std
分配的內(nèi)存空間總是以16的倍數(shù)增加(16~32~48......),我們之前分析源碼好像沒看到有做這個設(shè)定
其實上面這個方法只是可以用來計算一個結(jié)構(gòu)體對象所實際需要的內(nèi)存大小伟姐。[update]其實instanceSize()
-->alignedInstanceSize()
只是可以用來計算一個結(jié)構(gòu)體對象理論上(按照內(nèi)存對其規(guī)則)所需要分配的內(nèi)存大小收苏。
真正給實例對象完成分配內(nèi)存操作的是下面這個方法calloc()
這個方法位于蘋果源碼的libmalloc文件夾中。但是里面的代碼再往下深究愤兵,介于我目前的知識儲備以及專業(yè)出身(數(shù)學(xué)專業(yè))鹿霸,還是困難比較大。好在從一些大神那里得到了指點秆乳。
剛才文章開始懦鼠,我們討論到了結(jié)構(gòu)體的內(nèi)存對齊,這是針對數(shù)據(jù)結(jié)構(gòu)而言的屹堰。從系統(tǒng)層面來說肛冶,就以蘋果系統(tǒng)而言,出于對內(nèi)存管理和訪問效率最優(yōu)化的需要扯键,會實現(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)的庫,暫時不用對其過分解讀。
上面的NANO_MAX_SIZE
解釋中有個詞Buckets sized
沥匈,就是蘋果事先規(guī)劃好的內(nèi)存塊的大小要求蔗喂,針對nano
,內(nèi)存塊都被設(shè)定成16的倍數(shù)高帖,并且最大值是256缰儿。舉個例子,如果一個對象結(jié)構(gòu)體需要46個字節(jié)散址,那么系統(tǒng)會找一塊48字節(jié)的內(nèi)存塊分配給它用乖阵,如果另一個結(jié)構(gòu)體需要58個字節(jié),那么系統(tǒng)會找一塊64字節(jié)的內(nèi)存塊分配給它用预麸。
到這里瞪浸,應(yīng)該就可以基本上解釋清楚,為什么剛才student
結(jié)構(gòu)需要40個字節(jié)的時候吏祸,被分配到的內(nèi)存大小確實48個字節(jié)对蒲。至此,針對一個NSObject
對象占用內(nèi)存的問題贡翘,以及延伸出來的內(nèi)存布局蹈矮,以及其子類的占內(nèi)存問題,應(yīng)該就都可以得到解答了鸣驱。
OC對象的本質(zhì)(上):OC對象的底層實現(xiàn)
OC對象的本質(zhì)(中):OC對象的分類
OC對象的本質(zhì)(下):詳解isa&superclass指針
面試題解答
- 一個NSObject對象占用多少內(nèi)存泛鸟?
1)系統(tǒng)分配了16字節(jié)給NSObject對象(通過malloc_size
函數(shù)可以獲得)
2)NSObject對象內(nèi)部只使用了8個字節(jié)的空間,用來存放isa
指針變量(64位系統(tǒng)下踊东,可以通過class_getInstanceSize
函數(shù)獲得)