用于記錄iOS底層學(xué)習(xí),以備后續(xù)回顧
OC對(duì)象底層探索
前言
我們繼續(xù)來(lái)探索對(duì)象是如何申請(qǐng)、開(kāi)辟、優(yōu)化內(nèi)存大小的。
要想了解對(duì)象的內(nèi)存優(yōu)化首先要知道內(nèi)存對(duì)齊原則(理論加實(shí)踐一點(diǎn)點(diǎn)的搞懂)。
一、內(nèi)存對(duì)齊
1.1 內(nèi)存對(duì)齊的三個(gè)規(guī)則
- a. 數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)體(struct)(或聯(lián)合體(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如數(shù)組、結(jié)構(gòu)體等)的整數(shù)倍開(kāi)始(比如int為4字節(jié),則要從4的整數(shù)倍地址開(kāi)始存儲(chǔ))。
- b. 結(jié)構(gòu)體作為成員:如果一個(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ǔ))
- c. 最后判斷:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足要補(bǔ)齊
1.2 內(nèi)存對(duì)齊代碼示例探究
經(jīng)過(guò)實(shí)際探究,下列示例已涵蓋內(nèi)存對(duì)齊的全部原則并加以備注
struct DZStruct1 {
char a; // 1 [0]
double b; // 8 [8, 15]
int c; // 4 [16, 19]
short d; // 2 [20, 22]
} MyStruct1; // sizeof(MyStruct1) = 24字節(jié)
struct DZStruct2 {
double b; // 8 [0, 7]
char a; // 1 [8]
int c; // 4 [12, 15] 按順序從9開(kāi)始,但是起始位置需要從該成員大小的整數(shù)倍開(kāi)始,所以起始位置9需要往大了走,到12為4的整數(shù)倍
short d; // 2 [16, 17]
} MyStruct2; // sizeof(MyStruct2) = 24字節(jié),結(jié)構(gòu)體的總大小,必須是其內(nèi)部最大成員的整數(shù)倍,不足要補(bǔ)齊所以要從17補(bǔ)齊到24為8的整數(shù)倍
struct DZStruct3 {
char a; // 1 [0]
struct DZStruct1 struct1; // 24 [8, 31] 按順序從1開(kāi)始,但是起始位置需要從該結(jié)構(gòu)體(DZStruct1)內(nèi)部最大元素大小的整數(shù)倍開(kāi)始存儲(chǔ),所以從1往大了走,到8為8的整數(shù)倍
double b; // 8 [32, 39]
short d; // 2 [40, 41]
} MyStruct3; // sizeof(MyStruct3) = 48字節(jié)
1.3 字節(jié)基礎(chǔ)補(bǔ)充
a. 先簡(jiǎn)單補(bǔ)充一點(diǎn)字節(jié)相關(guān)基礎(chǔ)知識(shí):
sizeof()是運(yùn)算符,編譯的時(shí)候就是一個(gè)確定的數(shù)據(jù)會(huì)替換為常數(shù),返回的是一個(gè)類(lèi)型所占內(nèi)存的字節(jié)大小。

b. 了解獲取內(nèi)存的三個(gè)方法
-
sizeof是運(yùn)算符,編譯的時(shí)候就替換為常數(shù),返回的是一個(gè)類(lèi)型所占內(nèi)存的大小 -
class_getInstanceSize傳入一個(gè)類(lèi)對(duì)象,返回一個(gè)對(duì)象的實(shí)例至少需要多少內(nèi)存,它等價(jià)于sizeof,需要導(dǎo)入#import <objc/runtime.h> -
malloc_size返回系統(tǒng)實(shí)際分配的內(nèi)存大小,需要導(dǎo)入#import <malloc/malloc.h>
二、探索對(duì)象申請(qǐng)內(nèi)存和系統(tǒng)分配內(nèi)存
2.1 首先對(duì)無(wú)成員變量對(duì)象進(jìn)行探索
DZTeacher.h文件
#import <Foundation/Foundation.h>
@interface DZTeacher : NSObject
main.m文件
#import <Foundation/Foundation.h>
#import "DZTeacher.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZTeacher *p = [DZTeacher alloc];
NSLog(@"申請(qǐng)內(nèi)存大小為:%lu——-系統(tǒng)開(kāi)辟內(nèi)存大小為:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
}
return 0;
}
分析結(jié)果:無(wú)成員變量對(duì)象應(yīng)該分配內(nèi)存:isa的8字節(jié)(isa后續(xù)會(huì)進(jìn)行詳細(xì)探索)
實(shí)際打印結(jié)果 :
申請(qǐng)內(nèi)存大小為:8——-系統(tǒng)開(kāi)辟內(nèi)存大小為:16
class_getInstanceSize 內(nèi)部實(shí)現(xiàn)如下:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize(); // 直接返回字節(jié)對(duì)齊大小
}
回憶上一篇文章, alloc 初始化往下探索其中有一個(gè)方法_class_createInstanceFromZone,內(nèi)部會(huì)調(diào)用size_t size = cls->instanceSize(extraBytes),具體實(shí)現(xiàn)如下:
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; // 最小返回16
return size;
}
我們通過(guò)查看OC源碼可知,class_getInstanceSize是返回類(lèi)的成員變量占據(jù)內(nèi)存的大小,而malloc_size是獲取obj指針指向內(nèi)存的大小。
分析得出:系統(tǒng)在創(chuàng)建一個(gè)對(duì)象的時(shí)候,對(duì)象的isa指針占據(jù)8個(gè)字節(jié),但是系統(tǒng)會(huì)為其分配最少16字節(jié)的內(nèi)存空間,所以如果該對(duì)象沒(méi)有成員變量, class_getInstanceSize 會(huì)輸出 8 個(gè)字節(jié),malloc_size 會(huì)輸出最少 16 個(gè)字節(jié)。
2.2 添加成員變量后對(duì)象內(nèi)存探索
DZTeacher.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface DZTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;
main.m文件
#import <Foundation/Foundation.h>
#import "DZTeacher.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZTeacher *p = [DZTeacher alloc];
// isa ---- 8
p.name = @"Dezi"; // sizeof(p.name) = 8
p.age = 18; // 4
p.height = 185; // 8
p.hobby = @"女"; // 8
NSLog(@"%@",p);
NSLog(@"申請(qǐng)內(nèi)存大小為:%lu——-系統(tǒng)開(kāi)辟內(nèi)存大小為:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
}
return 0;
}
分析結(jié)果:
1. 成員變量應(yīng)該分配內(nèi)存:8字節(jié) + 4字節(jié) + 8字節(jié) + 8字節(jié) = 28字節(jié)
2. 再加上isa的8字節(jié) = 36字節(jié)(isa后續(xù)會(huì)進(jìn)行詳細(xì)探索)
3. 根據(jù)字節(jié)對(duì)齊原則最大8字節(jié),所以36往大走補(bǔ)充到40字節(jié)
4. 所以?xún)?nèi)存空間應(yīng)該分配40字節(jié)
5. 注意:對(duì)象開(kāi)辟空間的時(shí)候成員變量就會(huì)編譯進(jìn)來(lái),所以成員變量未賦值也會(huì)分配內(nèi)存
實(shí)際打印結(jié)果 :
申請(qǐng)內(nèi)存大小為:40——-系統(tǒng)開(kāi)辟內(nèi)存大小為:48
根據(jù)上方打印信息,我們的分析是對(duì)的,類(lèi)對(duì)象至少需要40字節(jié),那為什么實(shí)際分配內(nèi)存大小為48字節(jié)呢?下來(lái)我們繼續(xù)探索。
2.2 calloc探索
經(jīng)過(guò)對(duì)源碼的一步步探索,我們發(fā)現(xiàn),在obj = (id)calloc(1, size);這個(gè)方法的時(shí)候,對(duì)象內(nèi)存大小發(fā)生了變化


根據(jù)上圖我們發(fā)現(xiàn)calloc方法在malloc源碼里邊,那我們打開(kāi)新的源碼繼續(xù)分析:

之后進(jìn)入calloc流程,首先調(diào)用malloc_zone_calloc方法

在其內(nèi)部調(diào)用zone->calloc初始化并且返回了一個(gè)ptr指針

斷點(diǎn)在此處我們發(fā)現(xiàn)遞歸了,那我們?cè)谶@里打印zone->calloc,我們找到malloc.c文件249行的default_zone_calloc方法

加上斷點(diǎn)我們發(fā)現(xiàn)此處又是zone->calloc,繼續(xù)打印zone->calloc發(fā)現(xiàn)nano_malloc.c文件中878行的nano_calloc方法

在malloc的源碼中搜索nano_calloc,于nano_calloc.c文件中找到該方法,其中的核心代碼_nano_malloc_check_clear進(jìn)行內(nèi)存申請(qǐng),并且返回一個(gè)指針p

在_nano_malloc_check_clear內(nèi)部發(fā)現(xiàn)segregated_size_to_fit方法輸入的size是40,輸出的是48,所以這個(gè)方法是開(kāi)辟內(nèi)存的算法

對(duì)segregated_size_to_fit方法進(jìn)行分析,發(fā)現(xiàn)對(duì)齊原則是16字節(jié)對(duì)齊,所以輸入實(shí)際需要的40,經(jīng)過(guò)16字節(jié)對(duì)齊后輸出的為48


2.3 總結(jié)+流程圖
對(duì)象內(nèi)存大小的申請(qǐng)是按照8字節(jié)對(duì)齊,不滿16字節(jié)時(shí)按照16字節(jié)計(jì)算;若大于16字節(jié)時(shí),calloc實(shí)際開(kāi)辟內(nèi)存則是按照16字節(jié)對(duì)齊。
感覺(jué)8字節(jié)對(duì)齊是為了屬性之間內(nèi)存安全提高容錯(cuò)空間,16字節(jié)對(duì)齊是為了保證對(duì)象之間內(nèi)存安全提高容錯(cuò)空間。
