前言
在底層研究 - 對(duì)象的底層探索(上)已經(jīng)探索完對(duì)象alloc底層原理,對(duì)象的內(nèi)存對(duì)齊和結(jié)構(gòu)體的內(nèi)存對(duì)齊,同時(shí)也知道了結(jié)構(gòu)體內(nèi)順序?qū)Y(jié)構(gòu)體的內(nèi)存分配大小產(chǎn)生影響翁逞,接下來繼續(xù)探究對(duì)象的內(nèi)存分布
一些用到的lldb指令
- p/x 以十六進(jìn)制打印數(shù)據(jù)
- p/o 以八進(jìn)制打印數(shù)據(jù)
- p/t 以二進(jìn)制打印數(shù)據(jù)
- p/f 以浮點(diǎn)形式打印數(shù)據(jù)
- x/4gx 輸出對(duì)象的內(nèi)存地址,x/4gx中4代表輸出4個(gè)卿叽,g代表每一個(gè)是8字節(jié)大小彤断,x代表以16進(jìn)制打印漱贱。
1、影響對(duì)象內(nèi)存的因素
創(chuàng)建一個(gè)Person類妓笙,并且實(shí)例化一個(gè)對(duì)象并對(duì)其進(jìn)行賦值若河,發(fā)現(xiàn)打印的結(jié)構(gòu)是48.
@interface Person : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@property (nonatomic ,assign) char a;
@end
@implementation Person
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Person *p = [Person new];
p.name = @"小明";
p.hobby = @"boy";
p.hight = 1.8;
p.age = 18;
p.number = 123;
p.a = 5;
NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//打印結(jié)果:48
我們打個(gè)斷點(diǎn)看看p的內(nèi)存分布情況,由于第一個(gè)8字節(jié)是isa寞宫,直接從第二個(gè)8字節(jié)開始打印萧福。
通過打印結(jié)果可以發(fā)現(xiàn),第二個(gè)地址打印的值并不是我們賦值的成員變量的值辈赋,而且age鲫忍、number、a這三個(gè)成員變量的值并沒有發(fā)現(xiàn)钥屈。我們唯一的著手的就只有第二個(gè)莫名其妙的地址悟民,嘗試把它拆開分成三斷打印
可以發(fā)現(xiàn)age、number焕蹄、a這三個(gè)成員變量的值出現(xiàn)了逾雄,說明它們?cè)谕粋€(gè)內(nèi)存地址里阀溶。由此我們也可以得出一個(gè)結(jié)論腻脏,成員變量的值在存儲(chǔ)時(shí)自動(dòng)重新排序。那為什么這么做呢银锻?可以推測(cè)是為了優(yōu)化內(nèi)存永品。
對(duì)象的內(nèi)存是8字節(jié)對(duì)齊,a和age以及number總共占用1+4+2 = 7 個(gè)字節(jié)击纬,并沒有超出8字節(jié)鼎姐,可以放在同一個(gè)內(nèi)存地址內(nèi),從而節(jié)省了內(nèi)存的消耗更振。
2炕桨、對(duì)象的內(nèi)存分布
既然對(duì)象的屬性在賦值后會(huì)自動(dòng)重排,那對(duì)象本身的成員變量或者繼承自父類的屬性會(huì)不會(huì)也重排呢?
@interface Person : NSObject
{
@public
int age;
NSString *name;
NSString *sex;
int age1;
}
@end
@interface Person1 : NSObject
{
@public
int age;
NSString *name;
int age1;
NSString *sex;
}
@end
@interface Person2 : Person
{
@public
char a;
}
@end
@interface Person3 : Person1
{
@public
char a;
}
@end
@implementation Person
@end
@implementation Person1
@end
@implementation Person2
@end
@implementation Person3
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Person *person = [[Person alloc] init];
person->age1 = 18;
person->name = @"小明";
person->age = 19;
person->sex = @"男";
Person1* person1 = [[Person1 alloc] init];
person1->age1 = 18;
person1->name = @"小明";
person1->age = 19;
person1->sex = @"男";
Person2* person2 = [[Person2 alloc] init];
person2->age1 = 18;
person2->name = @"小明";
person2->age = 19;
person2->sex = @"男";
person2->a = 5;
Person3* person3 = [[Person3 alloc] init];
person3->age1 = 18;
person3->name = @"小明";
person3->age = 19;
person3->sex = @"男";
person3->a = 5;
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
從上面的打印結(jié)果可以得出兩個(gè)結(jié)論
- 通過對(duì)比person和person1可以發(fā)現(xiàn)肯腕,自己聲明的成員變量并不會(huì)被自動(dòng)重排順序献宫,并且在內(nèi)存中的分布是按照成員變量聲明的順序存儲(chǔ)的
- 通過對(duì)比person2和person3可以發(fā)現(xiàn),當(dāng)子類繼承自父類時(shí)实撒,子類的內(nèi)存是否被優(yōu)化姊途,取決于父類的成員變量位置,因?yàn)閜erson最后一個(gè)成員變量是int知态,不夠8字節(jié)捷兰,而person1是nsstring類型,已經(jīng)是8字節(jié)负敏。
person2的現(xiàn)象這其實(shí)是一個(gè)系統(tǒng)級(jí)別的優(yōu)化贡茅,并不是重排導(dǎo)致
那如果是屬性繼承的話,會(huì)不會(huì)觸發(fā)屬性重排呢?
@interface Person : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@end
@interface Person1 : Person
@property (nonatomic ,assign) char a;
@end
@implementation Person
@end
@implementation Person1
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Person1 *p = [Person1 new];
p.name = @"小明";
p.hobby = @"boy";
p.hight = 1.8;
p.age = 18;
p.number = 123;
p.a = 5;
NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
從上圖打印的結(jié)果可以發(fā)現(xiàn)事實(shí)上父類自動(dòng)生成的成員變量并沒有和子類自動(dòng)生成的成員變量一起被系統(tǒng)自動(dòng)重排順序顶考,而是各自進(jìn)行重排彤叉。這里出現(xiàn)這樣的原因是當(dāng)子類在繼承父類的數(shù)據(jù)結(jié)構(gòu)時(shí),父類是一塊連續(xù)的內(nèi)存空間村怪,子類是沒有辦法去修改父類的數(shù)據(jù)結(jié)構(gòu)的秽浇,也就是說系統(tǒng)在進(jìn)行屬性重排的時(shí)候只是基于某一個(gè)類,并不會(huì)把子類的成員變量和父類的成員變量重排在一起甚负。
3柬焕、位域和聯(lián)合體
接下來我們先講解下位域和聯(lián)合體。
- 位域:在一個(gè)結(jié)構(gòu)體中以位為單位來指定其成員所占內(nèi)存梭域,但指定的內(nèi)存大小不能超過該成員類型所占的最大內(nèi)存大小斑举。
struct Struct1 {
char a;
char b;
char c;
char d;
}struct1;
struct Struct2 {
// a: 位域名 4:位域長(zhǎng)度
char a : 1;
char b : 1;
char c : 1;
char d : 1;
}struct2;
一個(gè)正常的結(jié)構(gòu)體,它所占的內(nèi)存空間由它的數(shù)據(jù)結(jié)構(gòu)決定病涨,如果不使用位域富玷,struct1占用4個(gè)字節(jié),但struct2只占用了1個(gè)字節(jié)(實(shí)際上是1位既穆,8位1字節(jié))
struct Struct3 {
char a : 7;
char b : 1;
char c : 1;
char d : 1;
}struct3;
當(dāng)1個(gè)字節(jié)不夠存儲(chǔ)時(shí)赎懦,會(huì)自動(dòng)存入下一個(gè)字節(jié)
,也就是struct3占用了2個(gè)字節(jié)幻工。
- 聯(lián)合體:將幾種不同類型的變量存放到同一段內(nèi)存單元中励两,幾個(gè)變量互相覆蓋,聯(lián)合體的作用是節(jié)省一定的內(nèi)存空間囊颅,所占內(nèi)存取決于最大成員變量当悔,且必須是其最大成員變量(基本數(shù)據(jù)類型)的整數(shù)倍。
union Person {
char *name;
int number;
double height;
}p1;
根據(jù)規(guī)則計(jì)算出來:Person結(jié)構(gòu)體的內(nèi)存大小為8字節(jié)
當(dāng)聯(lián)合體中有數(shù)組時(shí):
union Person {
char a[7];
int number;
double height;
}p1;
聯(lián)合體Person中最大的成員變量是數(shù)組踢代,內(nèi)存占用7個(gè)字節(jié)盲憎,但因其不是基本數(shù)據(jù)類型,因此Person是double的整數(shù)倍胳挎,該聯(lián)合體占8個(gè)字節(jié)饼疙。
聯(lián)合體和結(jié)構(gòu)體的區(qū)別:
結(jié)構(gòu)體(struct)中所有變量是“共存”的,?聯(lián)合體(union)中是各變量是“互斥”的串远,只能存在?個(gè)宏多。struct內(nèi)存空間的分配是粗放的,不管?不?澡罚,全部分配伸但。這樣帶來的?個(gè)壞處就是對(duì)于內(nèi)存的消耗要??些。但是結(jié)構(gòu)體??的數(shù)據(jù)是完整的留搔。聯(lián)合體??的數(shù)據(jù)只能存在?個(gè)更胖,但優(yōu)點(diǎn)是內(nèi)存使?更為精細(xì)靈活,也節(jié)省了內(nèi)存空間。
4却妨、nonPointerIsa
有了聯(lián)合體和位域的知識(shí)饵逐,我們看下objc底層是怎么使用的,來到_class_createInstanceFromZone方法
進(jìn)入initInstanceIsa方法內(nèi)彪标,我們發(fā)現(xiàn)其內(nèi)部也是調(diào)用了initIsa方法
進(jìn)入initIsa方法內(nèi)倍权,我們發(fā)現(xiàn)了其內(nèi)部就是對(duì)對(duì)象的isa指針進(jìn)行初始化,同時(shí)我們發(fā)現(xiàn)了isa_t的數(shù)據(jù)類型
進(jìn)入isa_t發(fā)現(xiàn)它就是聯(lián)合體捞烟,根據(jù)我們掌握的聯(lián)合體知識(shí)可以發(fā)現(xiàn)它的目的是兼容舊版本的isa(Class cls)薄声。
如今的系統(tǒng)采用的是nonPointerIsa,相較于舊版本题画,它節(jié)省了內(nèi)存空間默辨。因?yàn)閷?duì)象的isa是一個(gè)8字節(jié)的Class類型的結(jié)構(gòu)體指針,主要是用來存儲(chǔ)對(duì)象所屬類對(duì)象的內(nèi)存地址的苍息,而存儲(chǔ)類對(duì)象的內(nèi)存地址不需要使用8字節(jié)這么大的內(nèi)存空間缩幸,所以系統(tǒng)就把一些與對(duì)象息息相關(guān)的信息也存儲(chǔ)到isa的內(nèi)存空間內(nèi),而nonPointerIsa的信息都存儲(chǔ)在ISA_BITFIELD這樣一個(gè)結(jié)構(gòu)體內(nèi)竞思。
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
這個(gè)nonPointerIsa的結(jié)構(gòu)體中有不少成員表谊,下面我們看下這些成員變量的作用:
//nonpointer:表示是否對(duì)isa指針開啟指針優(yōu)化,0:純isa指針衙四,1:不止是類對(duì)象地址铃肯,isa包含了類信息、對(duì)象的引用計(jì)數(shù)等
uintptr_t nonpointer : 1;
//has_assoc:關(guān)聯(lián)對(duì)象標(biāo)志位传蹈,0沒有,1存在
uintptr_t has_assoc : 1;
//has_cxx_dtor:該對(duì)象是否有C++或者Objc的析構(gòu)器步藕,如果有析構(gòu)函數(shù)惦界,則需要做析構(gòu)邏輯,如果沒有咙冗,則可以更快的釋放對(duì)象
uintptr_t has_cxx_dtor : 1;
//shiftcls:存儲(chǔ)類指針的值沾歪。開啟指針優(yōu)化的情況下,在arm64(真機(jī))架構(gòu)中用33位存儲(chǔ)類指針
uintptr_t shiftcls : 33;
//magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒有初始化的空間
uintptr_t magic : 6;
//weakly_referenced:標(biāo)志對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC的弱變量雾消,沒有弱引用的對(duì)象可以更快釋放
uintptr_t weakly_referenced : 1;
//unused:沒有使用(在之前舊版本灾搏,該位置表示 deallocating: 標(biāo)志對(duì)象是否正在釋放內(nèi)存)
uintptr_t unused : 1;
//has_sidetable_rc:是否需要使用sidetable來存儲(chǔ)引用計(jì)數(shù),當(dāng)對(duì)象引用計(jì)數(shù)大于10時(shí)立润,則需要借用該變量存儲(chǔ)進(jìn)位
uintptr_t has_sidetable_rc : 1;
//extra_rc:表示該對(duì)象的引用計(jì)數(shù)值狂窑,實(shí)際上是引用計(jì)數(shù)值減1,例如桑腮,如果對(duì)象的引用計(jì)數(shù)為10泉哈,那么extra_rc為9.如果引用計(jì)數(shù)大于10,則需要使用到上面的has_sidetable_rc
uintptr_t extra_rc : 19;
知道nonPointerIsa后,我們看下如何利用nonPointerIsa來獲取類對(duì)象丛晦!
5奕纫、利用isa得到類對(duì)象
通過nonPointerIsa的定義,我可以知道nonPointerIsa內(nèi)存儲(chǔ)的類對(duì)象地址空間在不同架構(gòu)下占用的位域分別為52烫沙、33匹层、44
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
.....
uintptr_t shiftcls_and_sig : 52; ......
# else
......
uintptr_t shiftcls : 33;
.....
# endif
# elif __x86_64__
.....
uintptr_t shiftcls : 44;
......
# endif
在源碼的main文件內(nèi)創(chuàng)建一個(gè)person對(duì)象,因?yàn)槭请娔X是x86_64架構(gòu)的锌蓄,所以對(duì)象占用的位域長(zhǎng)度是44位來存儲(chǔ)又固。我們可以分別使用兩種方式來獲取類對(duì)象:
-
ISA_MASK
我們可以直接用源碼提供的ISA_MASK & 上對(duì)象地址,就可以獲得類對(duì)象 -
位運(yùn)算
通過上面的分析我們已經(jīng)知道isa的內(nèi)存分配情況煤率,要取出完整的類對(duì)象信息仰冠,只需要把其余的數(shù)據(jù)全部清零即可。首先右移3位到最右邊蝶糯,左移20位(3 + 17)到最左邊洋只,再右移17位返回原來的位置
屬性是從低位往高位存,x86_64架構(gòu)又是小端模式昼捍,地址低位存放在低地址(高位存放在高地址)
例如:0x12345678
-> 大端:12 34 56 78
-> 小端:78 56 34 12
6识虚、new方法
我們經(jīng)常發(fā)現(xiàn)創(chuàng)建對(duì)象的方式除了alloc方法外,還有new方法妒茬,那么二者有什么區(qū)別呢担锤?
我們直接看下objc的源碼,找到new方法
通過源碼我們發(fā)現(xiàn)乍钻,new方法調(diào)用callAlloc函數(shù)后調(diào)用了init方法肛循,也就是說new方法本質(zhì)上就是alloc + init。
7银择、總結(jié)
- 對(duì)象??存儲(chǔ)了?個(gè)isa指針 + 成員變量的值多糠,isa指針是固定的,占8個(gè)字節(jié)浩考,所以影響對(duì)象內(nèi)存的只有成員變量(屬性會(huì)?動(dòng)?成帶下劃線的成員變量)
- 在對(duì)象的內(nèi)部是以8字節(jié)進(jìn)?對(duì)?的夹孔。
蘋果會(huì)?動(dòng)重排成員變量的順序,將占?不? 8 字節(jié)的成員挨在?起析孽,湊滿 8 字節(jié)搭伤,以達(dá)到優(yōu)化內(nèi)存的?的。 - 自己寫的成員變量的順序就按照書寫順序來的袜瞬,蘋果會(huì)重拍屬性的順序怜俐,但是在重排的時(shí)候不會(huì)考慮父類,父類和子類各自重排屬性吞滞。
4.結(jié)構(gòu)體(struct)中所有變量是“共存”的佑菩,?聯(lián)合體(union)中是各變量是“互斥”的盾沫,只能存在?個(gè)。 - 位域的寬度不能超過前?數(shù)據(jù)類型的最??度殿漠。
- nonPointerIsa是內(nèi)存優(yōu)化的?種?段赴精,通過位域和聯(lián)合體兼容了舊版本的isa和存儲(chǔ)其他的信息在isa的內(nèi)存空間中。
- new = alloc + init