OC對(duì)象(一)-- alloc和init底層到底在干嘛
OC對(duì)象(二)-- 內(nèi)存對(duì)齊和calloc中的16字節(jié)對(duì)齊
OC對(duì)象(三)-- isa結(jié)構(gòu)分析
內(nèi)存對(duì)齊初探
實(shí)例對(duì)象在內(nèi)存中的布局迄汛,是被系統(tǒng)優(yōu)化過(guò)的泉坐,不會(huì)按照屬性定義的順序在內(nèi)存中開(kāi)辟空間匆瓜。
舉個(gè)例子:
定義一個(gè)Person類喇聊,里面包括一些屬性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) char ch1;
@property (nonatomic, assign) char ch2;
@end
@implementation Person
@end
初始化實(shí)例對(duì)象衣赶,給屬性進(jìn)行賦值:
Person *p = [Person alloc];
p.name = @"DragonetZ";
p.nick = @"DZ";
p.age = 18;
p.ch1 = 'a';
p.ch2 = 'z';
NSLog(@"p:%@", p);
使用lldb指令x/4gx p
查看實(shí)例對(duì)象的內(nèi)存情況伍茄,p是實(shí)例對(duì)象的指針變量淹朋,打印結(jié)果如下:
(lldb) x/4gx p
0x600000683e20: 0x00000001061fb6c8 0x0000001200007a61
0x600000683e30: 0x00000001061f9018 0x00000001061f9038
- 第一個(gè)值0x00000001061fb6c8:實(shí)例對(duì)象的isa
- 第二個(gè)值0x0000001200007a61:里面存放著age偏塞、ch1唱蒸、ch2的值,都是使用十六進(jìn)制表示灸叼。0x12對(duì)應(yīng)十進(jìn)制18神汹。0x7a對(duì)應(yīng)的十進(jìn)制122,ASCII中對(duì)應(yīng)的就是‘z’古今,同理0x61對(duì)應(yīng)的97屁魏,就是‘a(chǎn)’
- 第三個(gè)值0x00000001061f9018:就是name屬性值‘DragonetZ’
- 第四個(gè)值0x00000001061f9038:就是nick屬性值‘DZ’
如圖中展示,內(nèi)存中的排序和類型屬性定義的順序不一致捉腥。
拓展-lldb命令解釋
上文用了lldb指令x/4gx p
氓拼,這里做一個(gè)簡(jiǎn)單解釋:
- p:讀取實(shí)例對(duì)象p的內(nèi)存。也就是讀取內(nèi)存的起始位置抵碟。
- 第一個(gè)x:是
memory read
指令的簡(jiǎn)寫(xiě)桃漾,讀取內(nèi)存作用 - 4g:從起始位置開(kāi)始,讀取4段
- 第二個(gè)x:代表的是16機(jī)制的方式讀取拟逮,同理可以切換成其他進(jìn)制模式:‘o’代表八進(jìn)制撬统,‘t’代表二進(jìn)制,‘d’代表十進(jìn)制唱歧。
打印內(nèi)容:
- 冒號(hào)左側(cè)宪摧,也就是圖中的紅色框中代表的是內(nèi)存地址粒竖,第一個(gè)地址也就是p的首地址颅崩。與
po p
的打印是相同過(guò)的柳爽。 - 冒號(hào)右側(cè)著蟹,是地址中的值呵萨。
內(nèi)存對(duì)齊原則
OC中俭令,實(shí)例對(duì)象其實(shí)就是struct類型灰嫉,因此我們研究一下struct是如何進(jìn)行內(nèi)存對(duì)齊的
簡(jiǎn)單的小案例
struct Struct1 {
double a;
char b;
int c;
short d;
}stu1;
struct Struct2 {
double a;
int b;
char c;
short d;
}stu2;
NSLog(@"%lu - %lu", sizeof(stu1), sizeof(stu2));
定義兩個(gè)struct贴汪,每個(gè)struct中都有幾個(gè)不同類型的成員鹿寨,打印兩個(gè)struct的內(nèi)存占用情況庐氮。
stu1占用24個(gè)字節(jié)瞧柔,stu2占用16字節(jié)漆弄。
此處可以使用下圖來(lái)自己先計(jì)算一下:
原理知識(shí)點(diǎn)
- struct第一個(gè)數(shù)據(jù)成員,從偏移量offset的0位開(kāi)始造锅。后續(xù)的成員從自身的整數(shù)倍的偏移位置開(kāi)始撼唾。
- 計(jì)算出來(lái)的總大小,需要是最大成員的整數(shù)倍哥蔚。
用stu1解釋說(shuō)明:
struct Struct1 {
double a;
char b;
int c;
short d;
}stu1;
- double a是第一個(gè)成員倒谷,占用8個(gè)字節(jié)蛛蒙,根據(jù)說(shuō)明,第一個(gè)成員offset是0渤愁,占用空間【0-7】
- char b牵祟,占用1個(gè)字節(jié),offset是8抖格,而且開(kāi)始位置8是需要占用空間1的整數(shù)倍诺苹,所以可以存放【8】
- int c,占用4個(gè)字節(jié)他挎,offset是9筝尾,開(kāi)始位置9不是需要占用空間4的整數(shù)倍,需要后移到整數(shù)倍12上存放办桨,占用的位置是【12-15】
- short d筹淫,占用2個(gè)字節(jié),offset是16呢撞,16是2的整數(shù)倍损姜,占用【16-17】
- 占用【0-17】共18個(gè)字節(jié),成員中最大的是double殊霞,占8字節(jié)摧阅,所以取8的整數(shù)倍,就是24字節(jié)绷蹲。
再來(lái)看看stu2:
struct Struct2 {
double a;
int b;
char c;
short d;
}stu2;
- double a棒卷,占用【0-7】
- int b,【8-11】祝钢,因?yàn)?是4的整數(shù)倍比规。
- char c,【12】
- shot d拦英,【14-15】蜒什,因?yàn)殚_(kāi)始位置13不是2的整數(shù)倍,因此從14開(kāi)始
- 占用【0-15】疤估,共16字節(jié)灾常,最大成員double是8字節(jié),取8整數(shù)倍铃拇,正好是16
擴(kuò)展-struct嵌套
如果struct a中有另一個(gè)struct b作為它的成員钞瀑,那么偏移量offset就取struct b中最大成員的整數(shù)倍開(kāi)始
struct Struct2 {
double a;
int b;
char c;
short d;
}stu2;
struct Struct3 {
char a;
short b;
struct Struct2 c;
}stu3;
struct Struct2前面分析結(jié)構(gòu):占用16個(gè)字節(jié),最大成員是8字節(jié)
分析stu3
- char a:【0】
- short b:【2-3】
- struct Struct2 c:找到8的整數(shù)倍作為開(kāi)始位慷荔,【8-23】
- 取最大成員8的整數(shù)倍雕什,也就是24
OC底層的優(yōu)化
上面的例子中stu1和stu2兩個(gè)struct可以說(shuō)是差不多,但是一個(gè)占用24,一個(gè)占用16监徘。但是我們?cè)贠C類的時(shí)候晋修,屬性順序是不受影響的。說(shuō)明蘋(píng)果底層是對(duì)我們內(nèi)存開(kāi)辟進(jìn)行優(yōu)化過(guò)的凰盔。這里可以通過(guò)示例對(duì)象內(nèi)存打印中可以發(fā)現(xiàn)墓卦,文章開(kāi)始的例子中:
Person類中屬性age、ch1户敬、ch2的值都存放在內(nèi)存中第二個(gè)位置(0x0000001200007a61)中落剪。
calloc
之前文章alloc和init底層到底在干嘛!中分析了alloc流程尿庐,先是用16字節(jié)對(duì)齊的方式計(jì)算出size忠怖,然后調(diào)用calloc函數(shù)來(lái)開(kāi)辟內(nèi)存空間
//16字節(jié)對(duì)齊獲取到size
size = cls->instanceSize(extraBytes);
//根據(jù)size開(kāi)辟內(nèi)存空間
obj = (id)calloc(1, size);
此時(shí)產(chǎn)生一個(gè)問(wèn)題,如果傳入的size沒(méi)有進(jìn)行16字節(jié)對(duì)齊抄瑟,也就是說(shuō)傳入的不是16字節(jié)的倍數(shù)凡泣,會(huì)是什么情況?
測(cè)試代碼
#import <malloc/malloc.h>
void *temp = calloc(1, 40);
NSLog(@"%lu", malloc_size(temp));
調(diào)用calloc方法皮假,第二個(gè)參數(shù)傳入40鞋拟,注意這個(gè)值不是16的倍數(shù)。
運(yùn)行結(jié)果:
通過(guò)結(jié)果可以看出惹资,calloc里面也會(huì)進(jìn)行16字節(jié)對(duì)齊贺纲,接下來(lái)我們來(lái)找找這16字節(jié)對(duì)齊的代碼。
查看源碼
首先先看看calloc函數(shù)在哪個(gè)源碼中褪测,用?+鼠標(biāo)左鍵猴誊,屬于malloc源碼中
源碼中大致的流程,如圖:
static MALLOC_INLINE size_t
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; // 16
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // (size + 16 -1)右移4位 SHIFT_NANO_QUANTUM=4
slot_bytes = k << SHIFT_NANO_QUANTUM; // 左移4位
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
這個(gè)函數(shù)就是16字節(jié)對(duì)齊的函數(shù):
- 先判斷size如果等于0侮措,就給一個(gè)默認(rèn)值16懈叹,
NANO_REGIME_QUANTA_SIZE
是個(gè)宏。 - 用size進(jìn)行計(jì)算萝毛,size+16-1项阴,將結(jié)果右移4位滑黔。然后在左移4位笆包。目的是將低4位抹零。通過(guò)左右位移4位略荡,來(lái)實(shí)現(xiàn)16字節(jié)對(duì)齊庵佣。