在探索OC對象的本質(zhì)之前,我們要明白Objective-C的代碼,底層的實現(xiàn)都是C/C++代碼。

而OC中的對象、類則是基于C/C++的結(jié)構(gòu)體來實現(xiàn)的。
我們可以通過將創(chuàng)建好的OC文件,轉(zhuǎn)化成C++文件來看一下OC對象的底層結(jié)構(gòu)。
OC代碼轉(zhuǎn)換成C++
通過命令行將OC的main.m文件轉(zhuǎn)換成C++,生成 main.cpp。
clang -rewrite-objc main.m -o main.cpp
/***rewrite代表 重寫
*-o代表 輸出
*cpp代表 c++(c plus plus)
**/
需要注意這種方式?jīng)]有指定運行平臺和架構(gòu)模式,我們可以通過命令行設(shè)置參數(shù),來指定運行平臺和架構(gòu)模式
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
/***xcrun代表 xcode
* iphoneos代表 運行在iPhone上
*-arch代表 架構(gòu)模式,arm64參數(shù)代表64位的架構(gòu)模式
**/
生成的main-arm64.cpp 文件就是main.m轉(zhuǎn)換c++后的文件,直接拖拽到工程中,就可以查看底層實現(xiàn)了。我們在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL即 implementation 的縮寫,代表實現(xiàn))。
struct NSObject_IMPL {
Class isa;
};
可以看到,NObject的底層實現(xiàn)就是一個結(jié)構(gòu)體。我們進一步查看Class
typedef struct objc_class *Class;
我們發(fā)現(xiàn)Class其實就是一個指針,指向了objc_class類型的結(jié)構(gòu)體。
OC對象內(nèi)存空間
通過以上探索我們可以得出總結(jié),OC中對象的底層實現(xiàn)都是通過C\C++的結(jié)構(gòu)體實現(xiàn)的。那么一個OC對象占據(jù)了多少內(nèi)存空間呢?下面讓我們用代碼實際驗證一下。

通過代碼我們創(chuàng)建了一個NSObject對象,分別調(diào)用了
class_getInstanceSize和malloc_size方法來查看占據(jù)的內(nèi)存空間。我們看到打印輸出了8和16,這是怎么回事呢?我們通過查看OC源碼可知,
class_getInstanceSize是返回類的成員變量占據(jù)內(nèi)存的大小。而malloc_size是獲取obj指針指向內(nèi)存的大小我們上文創(chuàng)建的NSObject對象obj的結(jié)構(gòu)體只有一個成員:isa指針,而指針在64位架構(gòu)中占8個字節(jié),所以第一次打印輸出了8。但是系統(tǒng)為obj對象分配了16個字節(jié)的內(nèi)存,第二次打印輸出16。

上圖中黃色區(qū)域系統(tǒng)為obj對象分配的16個字節(jié)的內(nèi)存,但是obj對象結(jié)構(gòu)體中只有一個isa指針,所以只占據(jù)了綠色區(qū)域的8個字節(jié)。這一點可以通過查看
alloc底層源碼也可以證實:
通過
alloc底層源碼可以看到,如果一個對象占據(jù)的內(nèi)存小于16時,則會直接返回16,也就是說,系統(tǒng)在為對象分配內(nèi)存空間時,最小為16個字節(jié)。
至此我們知道了系統(tǒng)在創(chuàng)建一個對象的時候會為其分配最少16字節(jié)的內(nèi)存空間,其中對象的isa指針占據(jù)8個字節(jié)。我們通過下面一道面試題繼續(xù)深入了解OC對象的底層本質(zhì)。
在64位環(huán)境下,下面的代碼會輸出什么?
/* Person */
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
/* Student */
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd %zd",
class_getInstanceSize([Person class]),
class_getInstanceSize([Student class])
);
}
return 0;
}
經(jīng)過代碼分析,這道面試題想問的其實就是一個Person對象和一個Student對象分別占據(jù)多少內(nèi)存空間。但是需要注意的是Person對象和Student對象分別有一個實例變量、Student繼承自Person對象。
經(jīng)過綜合上文簡單分析,我們可以簡單畫出Person對象和Student對象的內(nèi)存結(jié)構(gòu)示意圖:

我們可以看到,在Person對象結(jié)構(gòu)體中,有一個父類類型的結(jié)構(gòu)體指針和一個
int類型成員變量_age,分別占據(jù)內(nèi)存空間為8字節(jié)和4字節(jié)。根據(jù)底層源碼對象分配的最小內(nèi)存空間為16可得,Person對象占據(jù)的內(nèi)存空間為16。此處值得注意的是,根據(jù)內(nèi)存對齊,也可得出Person對象占據(jù)16個字節(jié)。
那么根據(jù)這個分析邏輯,Student對象占據(jù)的內(nèi)存空間是不是20或者24呢?答案當(dāng)然是不對,雖然Student結(jié)構(gòu)體中有Person類型的指針(16個字節(jié))和int類型成員變量_no(4個字節(jié)),但是要注意的是上面Person對象占據(jù)的16個字節(jié)是沒有完全占據(jù)完的,還留有4個字節(jié)的空白地址,系統(tǒng)會自動的把int類型成員變量_no放在這4個字節(jié)的空白地址中,所以Student對象也占據(jù)16個字節(jié)的內(nèi)存空間。

內(nèi)存對齊:
- 前面的地址必須是后面的地址整數(shù)倍,不是就補齊。
- 整個結(jié)構(gòu)體的地址必須是最大字節(jié)的整數(shù)倍。
OC對象的分類
OC對象分為三種:
實例對象(instance對象)
類對象(class對象)
元類對象(meta-class對象)
1、實例對象(instance對象)
實例對象(instance對象)就是通過類的alloc出來的對象,每次調(diào)用alloc都會產(chǎn)生新的實例對象。例如:
NSObjcet *obj1 = [[NSObjcet alloc] init];
NSObjcet *obj2 = [[NSObjcet alloc] init];
obj1和obj2都是NSObject的實例對象,但是它們是不同的兩個實例對象,分別占用兩塊不同的內(nèi)存地址。
實例對象在內(nèi)存中存儲的信息包括:
1、isa指針
2、其他成員變量

2、類對象(class對象)
類對象(class對象)就是通過class方法或者runtime的object_getClass方法得到的class對象
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
// runtime方法
Class objClass4 = object_getClass(obj1);
Class objClass5 = object_getClass(obj2);
objClass1-objClass5都是NSObject的類對象(class對象),且它們是同一個對象。
每個類在內(nèi)存中有且只有一個class對象
類對象在內(nèi)存中存儲的信息包括:
1、isa指針
2、superClass指針
3、類的屬性信息(@property),類的成員變量信息(ivar)
4、類的對象方法信息(instance method),類的協(xié)議信息(protocol)

3、元類對象(meta-class對象)
元類對象(meta-class對象)就是通過RunTime的object_getClass方法得到的對象
//通過RunTIme的API獲得元類對象
Class objectMetaClass = object_getClass([NSObject class]);
objectMetaClass就是NSObject的元類對象
每個類在內(nèi)存中有且只有一個元類對象
元類對象和類對象的內(nèi)存結(jié)構(gòu)是一樣的,但是用途不一樣,元類對象在內(nèi)存中存儲的信息包括:
1、isa指針
2、superClass指針
3、類的類方法信息(class method)

以上我們了解了實例對象、類對象和元類對象的含義以及包含的內(nèi)容,那么它們當(dāng)中的isa指針和superClass指針分別指向哪里呢?
isa指針的指向
isa指針指向用一張示意圖來簡單概括一下:

實例對象(instance對象)的isa指針指向class。當(dāng)調(diào)用對象方法時,通過實例對象的isa找到class,最后找到對象方法的實現(xiàn)進行調(diào)用
類對象(class對象)的isa指針指向meta-class。當(dāng)調(diào)用類方法時,通過類對象的isa找到meta-class,最后找到類方法的實現(xiàn)進行調(diào)用。
class的superClass指針的指向
class的superClass指針指向用一張示意圖來簡單概括一下:

圖中舉例Student繼承自Person,Person繼承自NSObject。
當(dāng)Student的實例對象要調(diào)用父類Person的對象方法時,會先通過
isa找到Student的class,然后通過class中的superClass找到父類Person的class,最后找到對象方法的實現(xiàn)進行調(diào)用。
meta-class對象的superClass指針指向

同上,當(dāng)Student的class要調(diào)用Person的類方法時,會先通過isa找到Student的meta-class,然后通過superClass找到Person的meta-class,最后找到類方法的實現(xiàn)進行調(diào)用。
這里當(dāng)然要提一下非常經(jīng)典的isa指向圖,做進一步的總結(jié):

1、instance的isa指向clas
2、class的isa指向meta-class
3、meta-class的isa指向基類的meta-class,基類的isa指向自己
4、class的superClass指向父類的class,如果沒有父類,則superClass指針為nil
5、meta-class的superClass指向父類的meta-class,基類的meta-class的superClass指向基類的class
6、instance調(diào)用對象方法的軌跡:通過isa找到class,方法不存在,就通過supercla逐層父類里找,有就實現(xiàn),如果找到基類仍沒有找到,就會拋出unrecognized selector sent to instance異常
7、class調(diào)用類方法的軌跡:通過isa找到meta-class,方法不存在,就通過superClass逐層父類里找。
補充:
相信很多人在查看源碼或者看一些底層博客的時候,經(jīng)常會看到下面一段代碼,來講述class的內(nèi)部結(jié)構(gòu):
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
這段源碼其實講述的也是class內(nèi)部結(jié)構(gòu),包含成員變量列表、方法列表、方法緩存以及協(xié)議列表。細心的人可能會發(fā)現(xiàn),這段代碼里面是有if判斷條件的:
#if !__OBJC2__
判斷條件是非OC2.0版本,也就是說在OC2.0之前的版本中,class底層的結(jié)構(gòu)體中包含上面代碼所講述的,但我們現(xiàn)在所用的最新版肯定是OC2.0版本了,所以這段代碼就不再使用了。
新的class底層的objc_class結(jié)構(gòu)體:

可以看到結(jié)構(gòu)體中只包含isa、superclass、cache和bits。而bits經(jīng)過& FAST_DATA_MASK之后,會得到struct class_rw_t這樣一個結(jié)構(gòu)體:

rw代表readwrite,可讀可寫
t代表table,列表
struct class_rw_t結(jié)構(gòu)體中就包含了方法列表、屬性列表以及協(xié)議列表,這些都是可讀可寫的。其中還包含一個struct class_ro_t的結(jié)構(gòu)體:

ro代表readonly,只讀
struct class_ro_t的結(jié)構(gòu)體中包含了instance對象占用的內(nèi)存空間、類名以及成員變量列表,當(dāng)然這些都是只讀的。
總結(jié):
一個NSObject對象占用多少內(nèi)存?
答:系統(tǒng)會為一個NSObject對象分配最少16個字節(jié)的內(nèi)存空間。一個指針變量所占用的大?。?4bit占8個字節(jié),32bit占4個字節(jié))
對象的isa指針指向哪里?
答:instance對象的isa指針指向class對象,class對象的isa指針指向meta-class對象,meta-class對象的isa指針指向基類的meta-class對象,基類自己的isa指針指向自己。
OC的類信息存放在哪里?
答:成員變量的具體值存放在實例對象(instance對象);對象方法,協(xié)議,屬性,成員變量信息存放在類對象(class對象);類方法信息存放在元類對象(meta-class對象)。
本文如果對你有所幫助,點亮喜歡支持一下
更多底層知識,掃碼關(guān)注公眾號
iOS進階
我的二維碼.jpg
