目錄
作為一個開發(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)存對齊原則:
結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第?個數(shù)據(jù)成員放在位置為0的地?,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員??或者成員的?成員??(只要該成員有?成員,?如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(?如int為4字節(jié),則要從4的整數(shù)倍地址開始)存儲。
如果?個結(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ù)倍開始儲)。
結(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