目錄
作為一個(gè)開(kāi)發(fā)者沉衣,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要郁副,這是一個(gè)我的iOS交流群:196800191,加群密碼:112233豌习,不管你是小白還是大牛歡迎入駐 霞势,分享BAT,阿里面試題、面試經(jīng)驗(yàn)斑鸦,討論技術(shù), 大家一起交流學(xué)習(xí)成長(zhǎng)草雕!
1. 概述
每個(gè)iOS開(kāi)始人員對(duì)OC語(yǔ)言并不陌生巷屿,雖然現(xiàn)在蘋(píng)果提倡swift開(kāi)發(fā),但是OC還是入門(mén)的必修課墩虹,平時(shí)開(kāi)發(fā)的時(shí)候嘱巾,我們通常就是調(diào)用各種API,很少探究其底層的原理诫钓,蘋(píng)果是如何在底層進(jìn)行封裝的呢旬昭?作為入行幾年的開(kāi)發(fā)者,還是有必要一探究竟菌湃。
OC是面向?qū)ο蟮恼Z(yǔ)言问拘,在代碼中最常見(jiàn)的就是創(chuàng)建一個(gè)對(duì)象了,那么對(duì)象是什么惧所,底層的結(jié)構(gòu)又是什么呢骤坐,是如何創(chuàng)建出來(lái)的呢?帶著這些問(wèn)題下愈,我們來(lái)開(kāi)始分析纽绍。
2. 對(duì)象是什么
對(duì)象在底層到底是個(gè)什么樣子呢?
在項(xiàng)目中創(chuàng)建一個(gè)GYMPerson類(lèi)势似,里面定義個(gè)name屬性和一個(gè)成員變量hobby拌夏,如下:
@interface GYMPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@end
然后通過(guò)命令行將main.m文件轉(zhuǎn)成c++文件main.cpp.
clang -rewrite-objc main.m -o main.cpp
轉(zhuǎn)換完成后打開(kāi)main.cpp文件僧著,此時(shí)找到了:
struct GYMPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *hobby;
NSString *_name;
};
這個(gè)就是GYMPerson在底層的形式,一個(gè)結(jié)構(gòu)體障簿,并且繼承了父類(lèi)的所有屬性盹愚。
另外我們注意到hobby沒(méi)有下劃線,而name則有下劃線卷谈,我們都知道成員變量在底層保持不變杯拐,不會(huì)生成一個(gè)帶下劃線的成員變量的,而name是一個(gè)屬性世蔗,在底層是會(huì)生成一個(gè)帶下劃線的成員變量的端逼,而且還會(huì)增加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); }
所以對(duì)象的本質(zhì)是什么污淋?毫無(wú)疑問(wèn)顶滩,結(jié)構(gòu)體。
3. 對(duì)象創(chuàng)建流程
我們?cè)诖a中調(diào)用alloc方法創(chuàng)建對(duì)象的時(shí)候寸爆,通常會(huì)經(jīng)歷以下幾個(gè)步驟礁鲁,簡(jiǎn)易圖如下:
當(dāng)在代碼中調(diào)用alloc的時(shí)候,例如[GYMPerson alloc]赁豆,那么在底層仅醇,代碼會(huì)先去哪里呢?
毫無(wú)疑問(wèn)魔种,當(dāng)?shù)谝淮蝿?chuàng)建GYMPerson對(duì)象的時(shí)候析二,是會(huì)來(lái)到下面這個(gè)方法的:
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
其實(shí)這個(gè)方法沒(méi)什么,那么再來(lái)看看callAlloc這個(gè)方法
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];
}
在這個(gè)方法中有個(gè)OBJC2判斷节预,現(xiàn)在底層的代碼全都是objc2的了叶摄,宏定義的值為1.
隨后遇到 fastpath(!cls->ISA()->hasCustomAWZ()) 判斷,因?yàn)檫@個(gè)類(lèi)是第一次創(chuàng)建對(duì)象安拟,類(lèi)還沒(méi)有初始化(懶加載)蛤吓,因此無(wú)法判斷該類(lèi)是否實(shí)現(xiàn)了allocWithZone方法,因而判斷也不成立糠赦,所以直接跳到下面allocWithZone的判斷会傲,但是callAlloc在調(diào)用的時(shí)候,傳入的allocWithZone是false拙泽,因此直接走到return唆铐,調(diào)用 [cls alloc] 。
+ (id)alloc {
return _objc_rootAlloc(self);
}
這一看奔滑,這也沒(méi)什么啊艾岂,別著急,繼續(xù)往下看
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
感覺(jué)被欺騙了朋其,怎么又回來(lái)了王浴?請(qǐng)注意脆炎,參數(shù)值不一樣了,此時(shí)的allocWithZone是true了氓辣。
此時(shí)如果沒(méi)有實(shí)現(xiàn)allocWithZone方法秒裕,那么 fastpath(!cls->ISA()->hasCustomAWZ()) 判斷則成立,進(jìn)入內(nèi)部的 fastpath(cls->canAllocFast()) 判斷钞啸,而這個(gè)判斷永遠(yuǎn)是false几蜻,如下:
bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
bool canAllocFast() {
return false;
}
因此代碼會(huì)走到 id obj = class_createInstance(cls, 0) 創(chuàng)建對(duì)象。
那么如果用戶實(shí)現(xiàn)了allocWithZone方法体斩,第二次調(diào)用callAlloc傳入的allocWithZone參數(shù)為true梭稚,此時(shí) fastpath(!cls->ISA()->hasCustomAWZ()) 判斷不成立,代碼直接走到 [cls allocWithZone:nil] 方法中絮吵,然后調(diào)用allocWithZone方法弧烤,隨后調(diào)用 _class_createInstanceFromZone 方法。
上面我們提到了一個(gè)創(chuàng)建對(duì)象的方法class_createInstance(cls, 0)蹬敲,那我們看看這個(gè)方法:
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
真是萬(wàn)變不離其中啊暇昂,最終又回到了我們實(shí)現(xiàn)allocWithZone方法后,底層調(diào)用的統(tǒng)一方法 _class_createInstanceFromZone伴嗡,下面我們來(lái)看一下這個(gè)方法急波。
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è)方法中zone為nil, fast經(jīng)判斷后是true,因此代碼會(huì)走到下面的代碼中:
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
這個(gè)if分支中就做了兩件事情瘪校,第一:在內(nèi)存中給這個(gè)對(duì)象開(kāi)辟空間幔崖,相當(dāng)于在小區(qū)里面申請(qǐng)了一套房子;第二:初始化isa渣淤,綁定對(duì)應(yīng)的類(lèi)信息,相當(dāng)于給這套房子弄個(gè)房本吉嫩,里面有具體的信息价认。
至于calloc和isa以后會(huì)講到,還有一個(gè)很重要的方法 cls->instanceSize(extraBytes) 馬上就會(huì)講到自娩,繼續(xù)往下看哦用踩!
以上則是一個(gè)對(duì)象的初始化過(guò)程,現(xiàn)在我們將上面的簡(jiǎn)易圖復(fù)雜化一下:
4. 對(duì)象空間大小及內(nèi)存對(duì)齊
上面我們主要探究了對(duì)象創(chuàng)建的流程忙迁,現(xiàn)在我們說(shuō)一下對(duì)象所需要空間的大小脐彩,以及字節(jié)對(duì)齊問(wèn)題。
還記得剛才說(shuō)過(guò)的方法嗎姊扔?
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;
}
這個(gè)方法主要計(jì)算對(duì)象所需要的內(nèi)存空間的大小惠奸,在了解如何計(jì)算之前,我們先看一下內(nèi)存對(duì)齊原則:
結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員恰梢,第?個(gè)數(shù)據(jù)成員放在位置為0的地?佛南,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員??或者成員的?成員??(只要該成員有?成員梗掰,?如說(shuō)是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開(kāi)始(?如int為4字節(jié),則要從4的整數(shù)倍地址開(kāi)始)存儲(chǔ)嗅回。
如果?個(gè)結(jié)構(gòu)?有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最?元素??的整數(shù)倍地址開(kāi)始存儲(chǔ).(struct a?存有struct b,b?有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開(kāi)始儲(chǔ))及穗。
結(jié)構(gòu)體的總??,必須是其內(nèi)部最?成員的整數(shù)倍.不?的要補(bǔ)齊绵载。
聽(tīng)起來(lái)是不是有些亂呢埂陆,來(lái),還是看代碼理解吧娃豹,下面有四個(gè)結(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焚虱,將其簡(jiǎn)化成一個(gè)公式:min(position, size),如果是存儲(chǔ)第一個(gè)元素培愁,那么直接放到0的位置著摔,從第二個(gè)元素開(kāi)始,采用這個(gè)公式定续,position是第二個(gè)及以后元素存儲(chǔ)的最小開(kāi)始位置谍咆,size則是元素的大小(比如int為4字節(jié))私股,公式的原理就是取position是size的最小整數(shù)倍的值作為存儲(chǔ)某一元素的開(kāi)始位置摹察。
我們看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位置的時(shí)候,只占用了1個(gè)字節(jié)倡鲸,此時(shí)position指向下一個(gè)可存儲(chǔ)的起始位置供嚎,也就是1,而下一個(gè)元素b是4字節(jié)峭状,那么position就往后移動(dòng)克滴,當(dāng)為4的時(shí)候(4為int(4字節(jié))的整數(shù)倍),存入b优床,則b存在4 5 6 7四個(gè)位置劝赔,此時(shí)position為8,下一個(gè)元素c胆敞,8個(gè)字節(jié)着帽,position正好是整數(shù)倍,于是開(kāi)始存c移层,則c存在8 9 10 11 12 13 14 15八個(gè)位置仍翰,此時(shí)position為16,正好是元素d(2字節(jié)的整數(shù)倍)观话,則d存在16 17兩個(gè)位置予借,這么一算結(jié)構(gòu)體Struct1一共占用了18個(gè)字符,但是別忘了原則3,結(jié)構(gòu)體整體大小是其內(nèi)部最大元素大小的整數(shù)倍蕾羊,iOS64位下最大的數(shù)據(jù)類(lèi)型占8字節(jié)喧笔,所以結(jié)構(gòu)體總大小應(yīng)是8的整數(shù)倍,那么比18大的最小整數(shù)倍即為24龟再,所以Struct1的總大小為24字節(jié)书闸。
如下入所示:
Struct2內(nèi)存對(duì)齊如下:
struct Struct2 {
double a; // 8 字節(jié)
int b; // 4 字節(jié)
char c; // 1 字節(jié)
short d; // 2 字節(jié)
} Struct2;
Struct3內(nèi)存對(duì)齊:
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中有個(gè)結(jié)構(gòu)體成員e利凑,這涉及到了內(nèi)存對(duì)齊的第二個(gè)原則浆劲。
其內(nèi)存對(duì)齊如下圖:
至于Struct4,感興趣的朋友可以自己算一算哀澈。
上面說(shuō)完了內(nèi)存對(duì)齊的原則以及結(jié)構(gòu)體內(nèi)存對(duì)齊牌借,下面回過(guò)頭看看創(chuàng)建對(duì)象時(shí)內(nèi)存大小是如何計(jì)算的。在創(chuàng)建對(duì)象的過(guò)程中割按,最后在calloc方法之前膨报,調(diào)用了 instanceSize(size_t extraBytes) 方法計(jì)算了對(duì)象申請(qǐng)的內(nèi)存空間大小,見(jiàn)下面的方法:
/**
方法中則調(diào)用 **alignedInstanceSize()** 方法進(jìn)行計(jì)算适荣,另外請(qǐng)注意下面還有個(gè)if判斷现柠,如果計(jì)算出來(lái)的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)存對(duì)齊的類(lèi)的屬性的總大小傳入 **word_align()** 方法中進(jìn)行對(duì)齊計(jì)算弛矛。
*/
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
// 返回類(lèi)的ivar中所有屬性的總大小够吩。
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
// 計(jì)算內(nèi)存對(duì)齊后對(duì)象需要的空間大小,詳見(jiàn)下面講解:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
方法解析:
我們知道NSObject對(duì)象有一個(gè)屬性丈氓,那就是isa周循,是一個(gè)指針類(lèi)型,所占空間大小為8字節(jié)万俗,如果創(chuàng)建一個(gè)NSObject湾笛,我們看看這個(gè)方法如何計(jì)算的。
首先看一下宏定義:
#define WORD_MASK 7UL
很顯然在64位下闰歪,WORD_MASK為7.
當(dāng)傳入的x為8的時(shí)候嚎研,那么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的時(shí)候置媳,經(jīng)過(guò)計(jì)算后得到的結(jié)果為8字節(jié)于樟。
是不是感覺(jué)有些巧合,都是8拇囊,好迂曲,那么假設(shè)傳入x=9,我們?cè)谟?jì)算一遍寥袭。
當(dāng)傳入的x為9的時(shí)候路捧,那么x + WORD_MASK為16关霸,其二進(jìn)制為:0001 0000
那么16 & ~7的計(jì)算為:
0001 0000
& 1111 1000
= 0001 0000
0001 0000的十進(jìn)制結(jié)果為16,由此可知杰扫,類(lèi)的屬性總空間大小為9队寇,經(jīng)過(guò)對(duì)齊后需要的空間為16.
由上面的分析可以,對(duì)象申請(qǐng)內(nèi)存空間的大小是8字節(jié)對(duì)齊計(jì)算的章姓。
經(jīng)過(guò)上面這一波計(jì)算佳遣,我們得到的內(nèi)存對(duì)齊后的數(shù)值就是對(duì)象創(chuàng)建的時(shí)候,向內(nèi)存申請(qǐng)的空間大小凡伊,那么計(jì)算機(jī)真的是按照這個(gè)數(shù)值開(kāi)辟的空間嗎零渐?請(qǐng)看下面章節(jié)。
5. 系統(tǒng)開(kāi)辟空間大小
計(jì)算機(jī)系統(tǒng)真的是按照對(duì)象申請(qǐng)的空間大小來(lái)開(kāi)辟空間嗎系忙?
答案:不是诵盼。
系統(tǒng)在calloc方法中,對(duì)于開(kāi)辟多大的空間银还,有自己的算法风宁。在探索calloc底層源碼的時(shí)候,有一個(gè)很重要的方法见剩,如下:
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;
}
還有兩個(gè)關(guān)鍵的宏定義:
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
方法解析:
假如方法中傳入的size為24(對(duì)象經(jīng)過(guò)內(nèi)存對(duì)齊后申請(qǐng)空間的大猩迸础),我們看一下這行:
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)為這個(gè)對(duì)象開(kāi)辟的實(shí)際空間的大小羹呵。
由上可知骂际,在callloc底層,系統(tǒng)將傳入的size右移4位冈欢,再左移4位歉铝,也就是16字節(jié)對(duì)齊。
下面舉個(gè)例子:
定義一個(gè)GYMDeveloper類(lèi)凑耻,繼承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
對(duì)于GYMDeveloper,如果要?jiǎng)?chuàng)建一個(gè)GYMDeveloper的實(shí)例對(duì)象香浩,很容易就會(huì)算出該對(duì)象所需要的空間大小类缤,即40字節(jié),不要忘了老祖宗NSObject還有isa指針邻吭,占8字節(jié)呢餐弱。
我們通過(guò)下面的方法測(cè)試一下:
- (void)instanceSizeFunction {
GYMDeveloper *developer = [GYMDeveloper alloc];
NSLog(@"對(duì)象申請(qǐng)的空間是:%lu字節(jié), 系統(tǒng)開(kāi)辟的空間是:%lu字節(jié)", class_getInstanceSize([developer class]), malloc_size((__bridge const void *)(developer)));
}
輸出結(jié)果為:
GYMDemo[51386:4021321] 對(duì)象申請(qǐng)的空間是:40字節(jié), 系統(tǒng)開(kāi)辟的空間是:48字節(jié)
綜上所述:對(duì)象申請(qǐng)空間的大小是8字節(jié)對(duì)齊計(jì)算的,而系統(tǒng)為對(duì)象開(kāi)辟空間是16字節(jié)對(duì)齊計(jì)算的。
寫(xiě)在最后:寫(xiě)文章不容易膏蚓,如果您覺(jué)得好就給個(gè)贊瓢谢,如果文章有問(wèn)題還請(qǐng)指正,轉(zhuǎn)載的話驮瞧,請(qǐng)標(biāo)注原文地址哦氓扛!
原文作者:Daniel_Coder
原文地址:https://blog.csdn.net/guoyongming925/article/details/108859202