[TOC]
前言
- 本文主要圍繞以下幾點內容展開討論;
對象的分配一定會占用8個字節嗎?如果它是char類型或者int類型的,占不滿8字節怎么辦,系統會不會對內存分配進行優化?
屬性的字節對齊和對象的字節對齊有何不同?
isa是個啥?里面包含啥?
共同體、位域的原理?
或運算的原理;
OC內存優化
探究字節對齊
舉個例子:
- int 4字節;
- char 1字節;
- nsstring 8字節;
思考
:由于字節對齊的原則
(不滿足8字節就要補齊),如果一個對象中都是int這樣的屬性那是不是要開辟很多個8字節的內存空間,這樣是不是很浪費?
首先還是要搞清楚,什么是字節對齊和字節補齊!請看下面;
- 該對象暴漏
2
個屬性,按照字節對齊的原則,int類型只占了4
個字節,需要補齊為8
個字節,所以該對象目前有三個屬性isa、name、age
,所以內存為該對象開辟了24
個字節;
- 該對象暴漏
3
個屬性,按照字節對齊的原則,int類型只占了4
個字節,需要補齊為8
個字節,但是這個對象的下個屬性是ch,ch本身占了1
個字節,系統默認將這一個字節排列到了age的后面,節約的空間,所以該對象目前有四個屬性isa、name、age、ch
,所以內存為該對象也是開辟了24
個字節;
通過輸出內存值的方式來驗證字節對齊和字節補齊的原理,請看下面;
-
C語言的結構體浪費內存空間
,它是按照寫入結構體的順序依次去開辟內存空間的,也就造成了下圖的情況,同樣的結構體
,一個開辟了24字節
、另一個開辟了16字節
;
-
OC語言對象中的屬性并不會因為屬性排列的先后順序,而導致內存分配時造成的浪費
,通過debug調試 在源碼size_t size = cls->instanceSize(extraBytes)
;出輸出兩個對象的size均為32
字節;
探究class_getInstanceSize
-
注意!:
這里要注意一點,在oc中輸出類的size直接使用NSLog(@"%lu",sizeof(p));是不準確的,因為它不能把類中所有的元素都計算出來。舉個例子;
-
疑問
:那如何打印系統實際為我們類開辟的的內存空間
是多大呢?請看下圖;
使用class_getInstanceSize
接口直接獲取內存大小;
-
疑問
:那這樣打印的值就是最終系統為我們開辟的內存大小嗎?請看下圖;
當對象中所有元素(包括isa
)所需要開辟的內存空間<16時,這時使用class_getInstanceSize
輸出的內存大小是要小于16字節的;
上面已經說了系統為我們類開辟內存大小可以用
class_getInstanceSize
來輸出,那我們類最開始希望內存為我們開辟多少內存
用什么來展示呢?請看下圖;
結論:class_getInstanceSize返回類對象至少需要多少空間
和malloc_size()返回的是實際分配的內存空間
是不一致的!!!!
探究malloc_size
探究
malloc_size
方法;
- 1、找到malloc_size方法;
alloc_size
的調用是在object-runtime_new.mm
中進行的,如下圖;但是alloc_size
的實現文件并沒暴漏出來,所以我們還是要去官網下載malloc_size
的源碼;
- 2、找到malloc_size源碼;源碼傳送門
- 3、debug源碼(1);
calloc
-->malloc_zone_calloc
-->ptr = zone->calloc(zone, num_items, size);
疑問?:
我擦?calloc
執行到最后還是calloc
怎么遞歸了?
- 3、debug源碼(2);
通過箭頭函數(屬性函數:屬性函數主要的目的是賦值,他的實現不在這里)
找到這個函數實現的方法請看下圖;
- 3、debug源碼(3);
- 4、debug源碼(4)-->
16字節對齊!
;
-
重點!!!:
這里我們再回顧下16字節對齊和8字節對齊的原理,16字節對齊就是 2^4-1
,8字節對齊就是2^3-1
- 5、debug源碼(5)-->
16字節對齊!
;
這也就說明了為什么,傳進來的size為40字節的內存空間會被系統修改為48,因為按照16字節對齊
的原則40需要補齊為48;
- 6、debug源碼(6)-->
為啥參照對象就是8字節對齊、系統分配就是16字節對齊?
NSLog(@"class_getInstanceSize返回類對象至少需要多少空間為%lu",class_getInstanceSize([p class]));
NSLog(@"malloc_size()返回的是實際分配的內存空間%lu",malloc_size((__bridge const void *)(p)));
8字節對齊
---------參考的是對象當中的屬性
16字節對齊
---------參考的是整個對象
- 7、debug源碼(7)-->
這樣就會造成一個現象,有可能至少需要的內存空間和實際分配的內存空間是不相同的
那為啥要多出來8位字節呢?
答案:
主要是為了對象之間的系統安全,因為你滿足8字節對齊只能保證對象中屬性之間的安全性,只有保證16字節對齊才能保證對象之間的安全性;系統開辟對象內存是連續的,滿足16字節對齊原則,防止內存越界
;(請看下圖)
探究編譯器優化
思考?:
那系統這么厲害是怎么優化代碼,讓運行的代碼達到優化的呢?
- 7、debug源碼(7)-->從
Debug
切換到release
后系統做了什么?(此篇文章只簡單介紹
)
編譯代碼
-->斷點
-->always show disassembly
查看匯編源碼會發現Debug模式下要比release模式下的代碼多很多;這就說明在release模式下系統會將臃腫的代碼進行整合和優化;
探究obj->initInstanceIsa(ISA)
1、探究ISA成員
isa是啥?:
ISA就是一個聯合體!
聯合體是干嘛的?
在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內存單元中。也就是使用覆蓋技術,幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內存的結構,在C語言中,被稱作“共用體”類型結構,簡稱共用體,也叫聯合體
。
舉個聯合體的例子!!!!!!!
注意:Union聯合體的成員之間共享內存空間。以isa_t
為例,cls
成員和bits
成員雖然不同,但是兩者的值實際在任何時候都是一致的。例如,isa.class = [NSString class]
指定了cls
指向NSString
類的內存地址,此時查看isa.bits
會發現其值為NSString
類的內存地址;反之,isa.bits = 0xFF
,則isa.class
的值變為255
。
isa
里面有啥?
cls
和bits
其中 struct
中的內容是為了解釋bits
的8*8位;
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
那這個8字節的isa對象里面都放了啥?
2、位域,有以下特性
什么是位域?
開辟完的內存空間你指定那些屬性放在哪個指定的區域,這就叫位域!
為什么引入位域的概念?
因為要節約內存空間!
因為一個字節有8位,如果你只用一個字節就可以放下這個成員的話,我就沒必要開辟太大的內存給你用,所以就用位域的概念在每個位域放置不同的成員,然后通過移位的方式來達到取值的效果;
二進制重排
與oc內存優化
的原理類似;
聯合體和位域
3、聯合體union
也成共用體,有以下特性
- union中可以定義多個成員,
union的大小由最大成員的大小決定
. - union成員共享同一塊大小的內存,一次只能使用其中的一個成員.
- 對union某一個成員賦值, 會覆蓋其他成員的值.
- union的存放順序是所有成員都從低地址開始存放的.
使用聯合體的好處
- 多個成員共用一塊內存
- 可讀性強
- 使用位運算提高數據存儲的效率
壞處
- 也是共用同一塊內存, 有可能會浪費一定的空間
證明
先創建一個對象 LGTank
@interface LGTank(){
// 聯合體
union {
char bits;
// 位域
struct {
char front : 1;
char back : 8;
char left : 1;
char right : 1;
};
} _direction;
}
1. union中可以定義多個成員, union的大小由最大成員的大小決定.
- struct中的值類型都為char時, 打印出來的大小為 1
bits 大小為1
結構體大小也為1
所以聯合體大小為1
- 將struct其中一個成員變量類型改為 short, 打印出來的大小為 2
bits 大小為1
結構體大小為2
所以聯合體大小為2
-
將bits類型改為 int, 打印出來的大小為 4
成員bits 大小為4
結構體大小為2
所以聯合體大小為4 -
將struct其中一個成員變量類型改為 int, 打印出來的大小為 4
成員
bits 大小為4
結構體大小為4
所以聯合體大小為4
2. 聯合體結構的分析
-
變量
char bits
這個使我們常見的定義變量的方式, 類型+變量名.
位域struct的分析
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
假如我們不知道有聯合體和位域這個稱謂, 這里定義了一個結構體, 沒有聲明結構體類型, 也沒有創建結構體變量, 那放在這里有什么用呢?
-
打印聯合體信息
聯合體
可以看出來存在兩個變量, 一個變量 bits, 一個匿名結構體變量, 值為(1, 0, 0, 0)
-
注釋這段代碼, 發現程序還是能夠正常運行, 也還是能輸出bits的值, 跟有沒有注釋這段代碼前的結果一模一樣.
聯合體
但是這段代碼輸出的聯合體重的變量只有一個 bits
3. 或運算和&運算原理
或運算有1得1;
0000 0000
0000 0001
或等于
0000 0001
- &運算有0得0、11得1;
0000 0000
1111 0110
或等于
0000 0000
4. 聯合體的賦值
對結構體的賦值一般都是通過位運算進行賦值
#define LGDirectionFrontMask (1 << 0)
#define LGDirectionBackMask (1 << 1)
#define LGDirectionLeftMask (1 << 2)
#define LGDirectionRightMask (1 << 3)
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LGDirectionFrontMask;
} else {
_direction.bits &= ~LGDirectionFrontMask;
}
}
因為聯合體共用的是一塊內存, 所以不能直接對bits賦一個數值, 這樣會導致結果超出預期范圍之外.
需要通過對面罩Mask做位運算來進行賦值
需要設置front為YES,
比如 LGDirectionFrontMask 的二進制為 0x00000001
- 最后一位一定是1, 所以需要跟最后一位或運算
- 現在需要改變 front位的值, 而不影響到其他位的值, 所以其他為要跟00使用或運算
需要設置front為NO,
- 最后一位是0, 必須執行與運算
- 而執行運算的時候, 不影響其他位域的值, 則需要跟1進行或運算. 從而可以推斷出跟bits進行與運算的數是0x11111110, 剛好等于~LGDirectionFrontMask
5. 共用空間
再來看這張圖, 在設置屬性的值的地方, 明明沒有設置結構體中front的值, 可是輸出的結果中卻顯示front=1, 就是我們設置的bits=1
猜想: 是否結構體中的值表示這個聯合體的某一位的值?
#define LGDirectionFrontMask 0b0000011
嘗試將面罩從0b1設置為0b11, 打印出來的結果如下
果然是表示這個聯合體前兩位的一個值
再次驗證
#define LGDirectionFrontMask 0b0000101
猜測結果 應該是 front位和left位值為 1
輸出的結果和我們預期是完全一模一樣的.
所以, 我們可以知道這個結構體在聯合體里面的作用相當于一個注釋的功能, 就是告訴使用這個聯合體的人, 這個聯合體每一位對應存的是什么信息, 你要按照結構體中標注的位域空間來進行讀取.
驚喜二, 上述驗證除了解釋聯合體的特性之外, 還輔助我解釋了小端序
這一特性
小端序
前面的筆記中說過小端模式
的特點是: 是指數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中。
當時一直不明白什么是數據的高字節/低字節, 什么又是內存的高地址/低地址
讓聯合體帶著我們來看
#define LGDirectionFrontMask 0b00000101
LGDirectionFrontMask這個常量, 左邊的是高字節, 這個很好理解, 不會理解的就當做十進制數的個十百千萬
的順序來理解就行.
而內存的低地址/高地址怎么理解呢? 我們平時通過x/4gx obj
來打印一個對象的屬性內存段地址時, 可以看到屬性的地址是依次增加的, 也就是說在前面的屬性的地址屬于低地址.
通過上面實例來分析: LGDirectionFrontMask最右邊的1, 表示這個字節的最低字節了, 而對應的聯合體union的內存地址中的 front 位--第一位, 也就是內存的最低地址, 從而驗證了數據的低字節保存的內存的低地址中
nice, 學習總是環環相扣, 當你對某個知識點理不清楚的時候, 不要鉆牛角尖, 去了解下一個知識點, 也許會柳暗花明又一村
聯合體和結構體的對比
- 分別定義一個相同結構的聯合體和結構體
// 聯合體
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
//結構體
struct {
char bits;
// 匿名結構體
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _word;
- 使用相同的面罩給他們賦值
#define LGDirectionFrontMask 0b0000101
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LGDirectionFrontMask;
_word.bits |= LGDirectionFrontMask;
} else {
_direction.bits &= ~LGDirectionFrontMask;
_word.bits &= ~LGDirectionFrontMask;
}
}
- 查看_direction和_word
(lldb) p tank->_direction
((anonymous union)) $0 = {
bits = '\x05'
= (front = '\x01', back = '\0', left = '\x01', right = '\0')
}
(lldb) p tank->_word
((anonymous struct)) $1 = {
bits = '\x05'
= (front = '\0', back = '\0', left = '\0', right = '\0')
}
在聯合體中, 可以看到我們定義的那個匿名結構體中有值, 從右往左讀分別是0x0101
, 而這個順序恰好是bits值(5)的二進制值, 這便解釋了位域
這個詞的含義, 結構體所在內存的每一位的值.
而在結構體中, 可以看到屬性bits
有值, 但是另一個屬性匿名結構體中的值并沒有任何變化, 說明這兩個屬性之間沒有任何關聯, 分別存在兩個不同的地方.
如何判斷ISA是cls還是 bits??
- 首先上面說了
cls
和bits
都屬于聯合體中的成員;那ISA它是哪種類型取決于什么呢?
答案
:取決于nonpointer是0還是1、如果是1代表是bits、如果是0則是cls;
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
總結
內存對齊的原則
- 原則一: 第一個數據成員放在offset為0的地方
- 原則二: 以后每個成員的起始位置為該成員大小的整數倍
- 原則三: 如果成員是結構體, 則這個成員的起始位置為結構體內部的最大成員的整數倍
- 原則四: 結構體總大小, 也就是sizeof的結果, 必須為內部成員(包括成員為結構體的內部成員)最大成員大小的整數倍, 不足要補齊.
內存對齊的思考
-
空間換時間
我們的內存按照8字節對齊, 計算機每次在讀數據的時候, 每次按照8字節的刻度讀取, 遠比逐個字節讀取的效率要高得多.
-
如何定義屬性保證結構體所占字節最小
這個系統會自動編排, 不需要我們關心成員的排列方式. 系統是如何自動編排的, 有興趣的時候可以研究下.
-
內存優化
內存對齊可以節約內存空間, 優化內存.
isa指針的結構也是優化內存的一種設計, 將不同的信息存在isa的不同位域, 避免使用過多的屬性 -
二進制重排
先將有意義的內存排列在一起, 優先進行加載, 對沒有用到的內存排列在其他地方等待加載, 以提高啟動速度.
[圖片上傳失敗...(image-bf64c7-1586161219748)]
屬性的字節對齊和對象的字節對齊有何不同?
屬性字節對齊8字節對齊
---------參考的是對象當中的屬性
對象字節對齊16字節對齊
---------參考的是整個對象
isa是個啥?里面包含啥?
isa
是一個聯合
體;它里面包含了兩個屬性;cls
和bits
,struct是對64位bits字節歸屬的解釋;
聯合體、位域的原理?他們搭配起來起到了什么作用?
或運算的原理;
- 或運算有1得1;
- &運算有0得0、11得1;