用于記錄iOS底層學(xué)習(xí),以備后續(xù)回顧
OC對象底層探索
前言
我們繼續(xù)來探索對象是如何申請熟尉、開辟慷荔、優(yōu)化內(nèi)存大小的耗跛。
要想了解對象的內(nèi)存優(yōu)化首先要知道內(nèi)存對齊原則(理論加實(shí)踐一點(diǎn)點(diǎn)的搞懂)菊碟。
一节芥、內(nèi)存對齊
1.1 內(nèi)存對齊的三個(gè)規(guī)則
- a. 數(shù)據(jù)成員對齊規(guī)則:結(jié)構(gòu)體(struct)(或聯(lián)合體(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方逆害,以后每個(gè)數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員头镊,比如數(shù)組、結(jié)構(gòu)體等)的整數(shù)倍開始(比如int為4字節(jié)魄幕,則要從4的整數(shù)倍地址開始存儲)拧晕。
- b. 結(jié)構(gòu)體作為成員:如果一個(gè)結(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ù)倍開始存儲)
- c. 最后判斷:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果欲鹏,必須是其內(nèi)部最大成員的整數(shù)倍机久,不足要補(bǔ)齊
1.2 內(nèi)存對齊代碼示例探究
經(jīng)過實(shí)際探究,下列示例已涵蓋內(nèi)存對齊的全部原則并加以備注
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開始赔嚎,但是起始位置需要從該成員大小的整數(shù)倍開始膘盖,所以起始位置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開始,但是起始位置需要從該結(jié)構(gòu)體(DZStruct1)內(nèi)部最大元素大小的整數(shù)倍開始存儲软棺,所以從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. 先簡單補(bǔ)充一點(diǎn)字節(jié)相關(guān)基礎(chǔ)知識:
sizeof()是運(yùn)算符,編譯的時(shí)候就是一個(gè)確定的數(shù)據(jù)會替換為常數(shù)喘落,返回的是一個(gè)類型所占內(nèi)存的字節(jié)大小茵宪。
b. 了解獲取內(nèi)存的三個(gè)方法
-
sizeof
是運(yùn)算符,編譯的時(shí)候就替換為常數(shù)瘦棋,返回的是一個(gè)類型所占內(nèi)存的大小 -
class_getInstanceSize
傳入一個(gè)類對象稀火,返回一個(gè)對象的實(shí)例至少需要多少內(nèi)存,它等價(jià)于sizeof
,需要導(dǎo)入#import <objc/runtime.h>
-
malloc_size
返回系統(tǒng)實(shí)際分配的內(nèi)存大小赌朋,需要導(dǎo)入#import <malloc/malloc.h>
二凰狞、探索對象申請內(nèi)存和系統(tǒng)分配內(nèi)存
2.1 首先對無成員變量對象進(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(@"申請內(nèi)存大小為:%lu——-系統(tǒng)開辟內(nèi)存大小為:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
}
return 0;
}
分析結(jié)果:無成員變量對象應(yīng)該分配內(nèi)存:isa的8字節(jié)(isa后續(xù)會進(jìn)行詳細(xì)探索)
實(shí)際打印結(jié)果 :
申請內(nèi)存大小為:8——-系統(tǒng)開辟內(nèi)存大小為:16
class_getInstanceSize
內(nèi)部實(shí)現(xiàn)如下:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize(); // 直接返回字節(jié)對齊大小
}
回憶上一篇文章, alloc
初始化往下探索其中有一個(gè)方法_class_createInstanceFromZone
箕慧,內(nèi)部會調(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;
}
我們通過查看OC源碼可知,class_getInstanceSize
是返回類的成員變量占據(jù)內(nèi)存的大小颠焦,而malloc_size
是獲取obj指針指向內(nèi)存的大小斩熊。
分析得出:系統(tǒng)在創(chuàng)建一個(gè)對象的時(shí)候,對象的isa指針占據(jù)8個(gè)字節(jié)伐庭,但是系統(tǒng)會為其分配最少16字節(jié)的內(nèi)存空間粉渠,所以如果該對象沒有成員變量, class_getInstanceSize
會輸出 8 個(gè)字節(jié)圾另,malloc_size
會輸出最少 16 個(gè)字節(jié)霸株。
2.2 添加成員變量后對象內(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(@"申請內(nèi)存大小為:%lu——-系統(tǒng)開辟內(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ù)會進(jìn)行詳細(xì)探索)
3. 根據(jù)字節(jié)對齊原則最大8字節(jié),所以36往大走補(bǔ)充到40字節(jié)
4. 所以內(nèi)存空間應(yīng)該分配40字節(jié)
5. 注意:對象開辟空間的時(shí)候成員變量就會編譯進(jìn)來集乔,所以成員變量未賦值也會分配內(nèi)存
實(shí)際打印結(jié)果 :
申請內(nèi)存大小為:40——-系統(tǒng)開辟內(nèi)存大小為:48
根據(jù)上方打印信息去件,我們的分析是對的,類對象至少需要40字節(jié)扰路,那為什么實(shí)際分配內(nèi)存大小為48字節(jié)呢尤溜?下來我們繼續(xù)探索。
2.2 calloc探索
經(jīng)過對源碼的一步步探索汗唱,我們發(fā)現(xiàn)宫莱,在obj = (id)calloc(1, size);
這個(gè)方法的時(shí)候,對象內(nèi)存大小發(fā)生了變化
根據(jù)上圖我們發(fā)現(xiàn)calloc方法在malloc源碼里邊哩罪,那我們打開新的源碼繼續(xù)分析:
之后進(jìn)入calloc流程授霸,首先調(diào)用malloc_zone_calloc方法
在其內(nèi)部調(diào)用zone->calloc
初始化并且返回了一個(gè)ptr指針
斷點(diǎn)在此處我們發(fā)現(xiàn)遞歸了巡验,那我們在這里打印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)存申請藏畅,并且返回一個(gè)指針p
在_nano_malloc_check_clear
內(nèi)部發(fā)現(xiàn)segregated_size_to_fit
方法輸入的size是40敷硅,輸出的是48,所以這個(gè)方法是開辟內(nèi)存的算法
對segregated_size_to_fit
方法進(jìn)行分析愉阎,發(fā)現(xiàn)對齊原則是16字節(jié)對齊绞蹦,所以輸入實(shí)際需要的40,經(jīng)過16字節(jié)對齊后輸出的為48
2.3 總結(jié)+流程圖
對象內(nèi)存大小的申請是按照8字節(jié)對齊榜旦,不滿16字節(jié)時(shí)按照16字節(jié)計(jì)算幽七;若大于16字節(jié)時(shí),calloc實(shí)際開辟內(nèi)存則是按照16字節(jié)對齊溅呢。
感覺8字節(jié)對齊是為了屬性之間內(nèi)存安全提高容錯(cuò)空間澡屡,16字節(jié)對齊是為了保證對象之間內(nèi)存安全提高容錯(cuò)空間。