寫在前面
在iOS之武功秘籍①:OC對象原理-上(alloc & init & new)一文中講了底層對象創(chuàng)建的流程颅湘,那么本文將來探索下對象中的屬性在內(nèi)存中的排列 -- 內(nèi)存對齊 和 malloc源碼分析
一涮雷、對象開辟內(nèi)存的影響因素(補(bǔ)充)
通過上一篇文章,我們已經(jīng)知道,創(chuàng)建一個(gè)對象,在經(jīng)過_class_createInstanceFromZone
方法時(shí),其內(nèi)部的size = cls->instanceSize(extraBytes)
能計(jì)算出創(chuàng)建這個(gè)對象所需的內(nèi)存空間大小.那么影響對象開辟內(nèi)存的因素是什么呢?
① 對于對象來說影響其內(nèi)存開辟的因素(影響對象字節(jié)對齊的因素) -- 對象的屬性(更確切的說應(yīng)該是對象的成員變量)
舉個(gè)??:-
當(dāng)我們的
TCJPerson
對象沒有其他屬性的時(shí)候,只有一個(gè)從父類NSObject
繼承過來的isa
時(shí),此時(shí)創(chuàng)建TCJPerson
對象所需的開辟的內(nèi)存空間大小為16字節(jié). -
當(dāng)我們增加一個(gè)
name
屬性時(shí),此時(shí)的size
大小還是 16(if (size < 16) size = 16
).
-
接著我們在增加一個(gè)
nickName
屬性,此時(shí)需要的size
大小為 32 (對象的字節(jié)對齊為16字節(jié),開辟對象的內(nèi)存大小必須是16的倍數(shù))
② 如何查看對象屬性在內(nèi)存中的顯示
測試代碼:注:如果對象創(chuàng)建了沒去賦值屬性——它會是內(nèi)存假地址
我們應(yīng)先給對應(yīng)的屬性賦值,不然的話他們在內(nèi)存中就是假的地址.因?yàn)閮?nèi)存是連續(xù)的,如果沒去用的話,在內(nèi)存中就是野指針.
②.1 第一種方式LLDB指令 -- 查看對象屬性在內(nèi)存中的顯示
LLDB調(diào)試命令等預(yù)備知識:
-
po
與p
:p表示"expression"——打印對象指針眉枕;而po是"expression -O"——打印對象本身
-
x 對象
表示以16進(jìn)制打印對象內(nèi)存地址(x表示memory read)因?yàn)閕OS是小端模式(數(shù)據(jù)的高字節(jié)保存在內(nèi)存的高地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的低地址中——反過來存放數(shù)據(jù))所以要倒著讀數(shù)據(jù)
-
x/8gx 對象
表示輸出8個(gè)16進(jìn)制的8字節(jié)地址空間(x表示16進(jìn)制,8表示8個(gè)仙畦,g表示8字節(jié)為單位,等同于x/8xg 對象
)
根據(jù)我們的計(jì)算機(jī)基礎(chǔ)和LLDB指令,可以發(fā)現(xiàn)
- 第一段是
isa
(從64位開始,isa
需要進(jìn)行一個(gè)位運(yùn)算& ISA_MASK
操作,而x86環(huán)境下,ISA_MASK
的值為0x0000000ffffffff8ULL
) - 第二段中
0x00000012
是18
-- 對應(yīng)age
,而62
替劈、61
分別是b
、a
的ASCII
編碼 - 第三段中po出來是
TCJ
--對應(yīng)name
- 第四段中po出來是
CJ
-- 對應(yīng)nickName
- 第五段是po出來是
185
-- 對應(yīng)height
查看控制臺輸出:
去掉聲明屬性查看控制臺輸出:
即TCJPerson
類中不聲明任何屬性:
提出問題
Q1:為什么成員變量的順序和我們聲明屬性的順序不同得滤?!Q2:sizeof陨献、class_getInstanceSize、malloc_size分別是什么懂更?
后面講解....
Q3:不是說對象最少為16字節(jié)眨业,為什么class_getInstanceSize還能輸出8字節(jié)?
后面講解...
②.2 第二種方式:實(shí)時(shí)查看內(nèi)存狀況Debug->Debug Workflow->View Memory(shift + Command +M)
一般不推薦用第二種方式.
二沮协、字節(jié)對齊
① sizeof龄捡、class_getInstanceSize、malloc_size
-
sizeof()
:是一個(gè)運(yùn)算符,不是函數(shù).傳入數(shù)據(jù)類型,輸出內(nèi)存大小,在編譯時(shí)確定.只與數(shù)據(jù)類型相關(guān)慷暂,與具體數(shù)值無關(guān)聘殖。(如:bool
2字節(jié),int
4字節(jié),對象(指針)8字節(jié)) -
class_getInstanceSize
:依賴于<objc/runtime.h>
,是runtime
提供的api
,用于獲取類的實(shí)例對象所占用的內(nèi)存大小
行瑞,并返回具體的字節(jié)數(shù)奸腺,其本質(zhì)就是獲取實(shí)例對象中成員變量的內(nèi)存大小
(8字節(jié)對齊) -
malloc_size
:依賴于<malloc/malloc.h>
,返回系統(tǒng)實(shí)際分配的內(nèi)存大小
(16字節(jié)對齊)
前面的打印也就得以驗(yàn)證.
在來總結(jié)一波:
-
sizeof
:計(jì)算類型占用的內(nèi)存大小
血久,其中可以放基本數(shù)據(jù)類型
突照、對象
、指針
- 對于類似于
int
這樣的基本數(shù)據(jù)而言氧吐,sizeof
獲取的就是數(shù)據(jù)類型占用的內(nèi)存大小讹蘑,不同的數(shù)據(jù)類型所占用的內(nèi)存大小是不一樣的 - 而對于類似于
NSObject
定義的實(shí)例對象而言,其對象類型的本質(zhì)就是一個(gè)結(jié)構(gòu)體(即 struct objc_object)的指針
副砍,所以sizeof(objc)
打印的是對象objc的指針大小
衔肢,我們知道一個(gè)指針的內(nèi)存大小是8字節(jié),所以sizeof(objc)
打印是 8.注意:這里的8字節(jié)與isa
指針一點(diǎn)關(guān)系都沒有;眙帷=侵琛! - 對于指針而言,
sizeof
打印的就是8邦尊,因?yàn)橐粋€(gè)指針的內(nèi)存大小是8字節(jié).
- 對于類似于
class_getInstanceSize
:計(jì)算對象實(shí)際占用的內(nèi)存大小背桐,這個(gè)需要依據(jù)類的屬性而變化,如果自定義類沒有自定義屬性蝉揍,僅僅只是繼承自NSObject
链峭,則類的實(shí)例對象實(shí)際占用的內(nèi)存大小是8,遵循8字節(jié)對齊.malloc_size
:計(jì)算對象實(shí)際分配的內(nèi)存大小又沾,這個(gè)是由系統(tǒng)完成的.可以從上面的打印結(jié)果看出弊仪,實(shí)際分配的和實(shí)際占用的內(nèi)存大小并不相等.
② 對象的內(nèi)存對齊
我們知道就對象整體而言,蘋果系統(tǒng)采用16字節(jié)對齊開辟內(nèi)存大小杖刷,提高系統(tǒng)存取性能励饵。
那么對于對象內(nèi)部呢?
- 對象的本質(zhì)是結(jié)構(gòu)體,這個(gè)在后續(xù)篇章中我們會詳細(xì)講解.所以研究對象內(nèi)部的內(nèi)存滑燃,就是研究結(jié)構(gòu)體的內(nèi)存布局.
- 內(nèi)存對齊目的:最大程度提高資源利用率.
③ 結(jié)構(gòu)體內(nèi)存對齊
搞個(gè)??瞧瞧:輸出結(jié)果: CJStruct1-24 CJStruct2-16 CJStruct3-32 CJStruct4-24
.
從打印結(jié)果我們可以看出一個(gè)問題役听,兩個(gè)結(jié)構(gòu)體乍一看,沒什么區(qū)別表窘,其中定義的變量 和 變量類型都是一致的典予,唯一的區(qū)別只是在于定義變量的順序不一致
,那為什么他們做占用的內(nèi)存大小不相等呢乐严?結(jié)構(gòu)體內(nèi)部的元素排序影響內(nèi)存大小.其實(shí)這就是iOS中的內(nèi)存字節(jié)對齊現(xiàn)象.
結(jié)構(gòu)體內(nèi)存對齊規(guī)則
每個(gè)特定平臺上的編譯器都有自己的默認(rèn)“對齊系數(shù)”(也叫對齊模數(shù)).程序員可以通過預(yù)編譯命令#pragma pack(n)
瘤袖,n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”.在iOS
中麦备,Xcode
默認(rèn)為#pragma pack(8)
孽椰,即8字節(jié)對齊
注意:這里的8字節(jié)對齊是結(jié)構(gòu)體內(nèi)部對齊規(guī)則,對象在系統(tǒng)中對外實(shí)際分配的空間是遵循16字節(jié)對齊原則凛篙。
【三條內(nèi)存對齊規(guī)則】:
- 數(shù)據(jù)成員的對齊規(guī)則可以理解為
min(m, n)
的公式, 其中m
表示當(dāng)前成員的開始位置,n
表示當(dāng)前成員所需位數(shù).如果滿足條件m
整除n
(即m % n == 0
),n
從m
位置開始存儲, 反之繼續(xù)檢查m+1
能否整除n
, 直到可以整除, 從而就確定了當(dāng)前成員的開始位置. - 數(shù)據(jù)成員為結(jié)構(gòu)體:當(dāng)結(jié)構(gòu)體嵌套了結(jié)構(gòu)體時(shí)黍匾,作為數(shù)據(jù)成員的結(jié)構(gòu)體的自身長度作為外部結(jié)構(gòu)體的最大成員的內(nèi)存大小(即在確定復(fù)合類型成員的偏移位置時(shí)則是將復(fù)合類型作為整體看待),且結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲.比如結(jié)構(gòu)體a嵌套結(jié)構(gòu)體b呛梆,b中有char锐涯、int、double等填物,那b應(yīng)該從8的整數(shù)倍開始存儲.
- 最后結(jié)構(gòu)體的內(nèi)存大小必須是結(jié)構(gòu)體中最大成員內(nèi)存大小的整數(shù)倍纹腌,不足的需要補(bǔ)齊.
iOS基礎(chǔ)數(shù)據(jù)類型占用的字節(jié)數(shù)表
利用結(jié)構(gòu)體對齊規(guī)則來分析前面的??
結(jié)構(gòu)體CJStruct1
內(nèi)存大小計(jì)算:
- 變量a:占8個(gè)字節(jié),從0開始滞磺,此時(shí)min(0升薯,8),即 0-7 存儲 a
- 變量b:占1個(gè)字節(jié)击困,從8開始涎劈,此時(shí)min(8广凸,1),8能整除1蛛枚,谅海,即 8 存儲 b
- 變量c:占4個(gè)字節(jié),從9開始蹦浦,此時(shí)min(9扭吁,4),9不能整除4盲镶,繼續(xù)往后移動侥袜,直到min(12,4)徒河,從12開始即 12-15 存儲 c
- 變量d:占2個(gè)字節(jié)系馆,從16開始,此時(shí)min(16, 2)顽照,16可以整除2,即16-17 存儲 d
因此CJStruct1
的需要的內(nèi)存大小為 18 字節(jié)闽寡,而CJStruct1
中最大變量的字節(jié)數(shù)為8代兵,所以CJStruct1
實(shí)際的內(nèi)存大小必須是 8 的整數(shù)倍,18向上取整到24爷狈,主要是因?yàn)?4是8的整數(shù)倍植影,所以sizeof(CJStruct1)
的結(jié)果是 24.
結(jié)構(gòu)體CJStruct2
內(nèi)存大小計(jì)算:
- 變量a:占8個(gè)字節(jié),從0開始涎永,此時(shí)min(0思币,8),即 0-7 存儲 b
- 變量b:占4個(gè)字節(jié)羡微,從8開始谷饿,此時(shí)min(8,4)妈倔,8可以整除4博投,即 8-11 存儲 c
- 變量c:占1個(gè)字節(jié),從12開始盯蝴,此時(shí)min(12, 1)毅哗,12可以整除1,即12 存儲 d
- 變量d:占2個(gè)字節(jié)捧挺,從13開始虑绵,此時(shí)min(13,2)闽烙,13不能整除2翅睛,繼續(xù)往后移動,直到min(14,2)宏所,從14開始即 14-15 存儲 c
因此CJStruct2
的需要的內(nèi)存大小為 16字節(jié)酥艳,而CJStruct2
中最大變量的字節(jié)數(shù)為8,所以CJStruct2
實(shí)際的內(nèi)存大小必須是 8 的整數(shù)倍爬骤,而 16 正好是 8 的整數(shù)倍充石,所以sizeof(CJStruct2)
的結(jié)果是 16.
結(jié)構(gòu)體CJStruct3
內(nèi)存大小計(jì)算:
- 變量e:占4個(gè)字節(jié),從0開始霞玄,此時(shí)min(0骤铃,4),即 0-3 存儲 e
- 結(jié)構(gòu)體成員
CJStruct1
:CJStruct1
是一個(gè)結(jié)構(gòu)體,占24字節(jié).根據(jù)內(nèi)存對齊原則二坷剧,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍開始存儲惰爬,而CJStruct1中最大的成員大小為8,所以CJStruct1要從8的整數(shù)倍開始惫企,當(dāng)前是從4開始撕瞧,所以不符合要求,需要往后移動到8狞尔,8是8的整數(shù)倍丛版,符合內(nèi)存對齊原則,所以 8-31 存儲 CJStruct1.
因此CJStruct3需要的內(nèi)存大小為 32 字節(jié)偏序,而CJStruct3 中最大變量為CJStruct1, 其最大成員內(nèi)存字節(jié)數(shù)為8页畦,根據(jù)內(nèi)存對齊原則,所以 CJStruct3 實(shí)際的內(nèi)存大小必須是 8 的整數(shù)倍研儒,32正好是8的整數(shù)倍豫缨,所以 sizeof(CJStruct3) 的結(jié)果是 32.
結(jié)構(gòu)體CJStruct4
內(nèi)存大小計(jì)算:
- 變量e:占8個(gè)字節(jié),從0開始端朵,此時(shí)min(0好芭,8),即 0-7 存儲 e
- 結(jié)構(gòu)體成員
CJStruct2
:CJStruct2
是一個(gè)結(jié)構(gòu)體,占16字節(jié).根據(jù)內(nèi)存對齊原則二逸月,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍開始存儲栓撞,而CJStruct2
中最大的成員大小為8,所以CJStruct2
要從8的整數(shù)倍開始碗硬,當(dāng)前是從8開始瓤湘,符合要求,符合內(nèi)存對齊原則恩尾,所以 8-23 存儲 CJStruct2.
因此CJStruct4
需要的內(nèi)存大小為 24 字節(jié)弛说,而CJStruct4
中最大變量為CJStruct2
, 其最大成員內(nèi)存字節(jié)數(shù)為8,根據(jù)內(nèi)存對齊原則翰意,所以CJStruct4
實(shí)際的內(nèi)存大小必須是 8 的整數(shù)倍木人,24正好是8的整數(shù)倍信柿,所以sizeof(CJStruct4)
的結(jié)果是 24.
④ 內(nèi)存優(yōu)化(屬性重排)
如果按照對象默認(rèn)聲明的屬性順序進(jìn)行內(nèi)存分配,在進(jìn)行屬性的8字節(jié)對齊時(shí)會浪費(fèi)大量的內(nèi)存空間醒第,所以這里系統(tǒng)會把對象的屬性重新排列渔嚷,以此來最大化利用我們的內(nèi)存空間
驗(yàn)證①:
就CJStruct1
與CJStruct2
而言,他們的成員屬性一樣,只是他們之間的屬性排列位置不同,他們分別占用不同的內(nèi)存.
驗(yàn)證②:
就前面 -- 如何查看對象屬性在內(nèi)存中的顯示 中的例子也驗(yàn)證了這一點(diǎn).
我們聲明TCJPerson
的屬性的順序是:isa(繼承NSObject) -> name(NSString) -> nickName(NSString) ->age(int) -> height(long) -> c1(char) -> c2(char);
而實(shí)際分配內(nèi)存時(shí)的屬性順序是:isa(繼承NSObject) ->age(int) -> c2(char) -> c1(char) -> nickName(NSString) -> name(NSString)-> height(long),并且將age 和 c1 及 c2 存放在了一個(gè)塊區(qū). 這就是蘋果的內(nèi)存優(yōu)化的體現(xiàn).
⑤小彩蛋
- 對于對象之間,系統(tǒng)面對的對象太多稠曼,系統(tǒng)為了防止容錯(cuò)形病,采用的是16字節(jié)對齊的內(nèi)存,給對象留足夠間距,避免越界訪問(所以malloc_size讀取的都是16的倍數(shù))
- 但為了避免浪費(fèi)太多內(nèi)存空間,系統(tǒng)會在每個(gè)對象內(nèi)部進(jìn)行屬性重排霞幅,并使用8字節(jié)對齊漠吻,使單個(gè)對象占用的資源盡可能小.(所以class_getInstanceSize讀取的都是8的倍數(shù))
三、malloc源碼輔助分析
通過上一篇文章,我們已經(jīng)知道,創(chuàng)建一個(gè)對象,在經(jīng)過_class_createInstanceFromZone
方法時(shí),其內(nèi)部的obj = (id)calloc(1, size)
方法是根據(jù)計(jì)算好的空間大小size
(如size = 40
),去系統(tǒng)申請空間,并返回地址指針的.
我們發(fā)現(xiàn)點(diǎn)擊calloc
進(jìn)入內(nèi)部司恳,只能看到calloc
聲明.無法再繼續(xù)前進(jìn)了
我們可以看到calloc
的聲明是在malloc源碼中.
打開我為你們準(zhǔn)備好的可編譯的malloc源碼.
① calloc
在libmalloc
源碼中新建target
途乃,按照objc
源碼中的調(diào)用方式操作:
② malloc_zone_calloc
之后進(jìn)入calloc
流程,進(jìn)行具體的內(nèi)存開辟扔傅,在使用calloc
申請內(nèi)存的過程中耍共,首先調(diào)用malloc_zone_calloc
方法
根據(jù)return ptr
可知ptr
是重點(diǎn),但是ptr = zone->calloc(zone, num_items, size)
;跟進(jìn)去會看到讓人一串摸不到頭腦的代碼,而且到此源碼還無法繼續(xù)跟進(jìn)了:
③ default_zone_calloc
那么重點(diǎn)來了A匀;帷!想要繼續(xù)跟進(jìn)源碼邢享,可以通過以下方法:
方法一: —— 分析zone
已知zone
是malloc_zone_t
類型的,在第二步中retval = malloc_zone_calloc(default_zone, num_items, size)
;中傳遞的第一個(gè)參數(shù)zone
又是default_zone
淡诗,跟蹤進(jìn)去會發(fā)現(xiàn)它是一個(gè)靜態(tài)變量
static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone;
static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_zone")))
__attribute__((aligned(PAGE_MAX_SIZE))) = {
NULL,
NULL,
default_zone_size,
default_zone_malloc,
default_zone_calloc,
default_zone_valloc,
default_zone_free,
default_zone_realloc,
default_zone_destroy,
DEFAULT_MALLOC_ZONE_STRING,
default_zone_batch_malloc,
default_zone_batch_free,
&default_zone_introspect,
10,
default_zone_memalign,
default_zone_free_definite_size,
default_zone_pressure_relief,
default_zone_malloc_claimed_address,
};
初步推測zone->alloc是default_zone_calloc
方法二: —— 控制臺打印
有時(shí)候打印也是閱讀源碼的一種方法——由打印可知實(shí)際調(diào)用default_zone_calloc
方法三: —— 按住control + step into骇塘,進(jìn)入calloc的源碼
加上圖中的斷點(diǎn),來到斷點(diǎn)后:按住control + step into
同樣也會來到:
這其中有兩個(gè)非常重要的操作:
- 創(chuàng)建真正的
zone
,即runtime_default_zone
方法 - 使用真正的
zone
進(jìn)行calloc
④ nano_calloc
繼續(xù)打印zone->calloc
,得到提示nano_calloc
:
在malloc
的源碼中搜索nano_calloc
韩容,于nano_malloc.c
文件中找到該方法款违,其中的核心代碼_nano_malloc_check_clear
,進(jìn)行內(nèi)存申請群凶,并且返回一個(gè)成熟的指針ptr.
⑤ _nano_malloc_check_clear
分析:這個(gè)方法中有三個(gè)return和一句注釋/* FALLTHROUGH to helper zone */——進(jìn)入輔助區(qū)域插爹,即正常情況下走if判斷(如果要開辟的空間小于 NANO_MAX_SIZE 則進(jìn)行nanozone_t的malloc)NANO_MAX_SIZE=256
⑥ segregated_size_to_fit
進(jìn)入_nano_malloc_check_clear
特姐,此時(shí)此刻看到這么長的一段代碼也不用慌張膘格,if-else
只走其一.再仔細(xì)想想嗦玖,我們是帶著目的來看源碼的——malloc_size
中的48是怎么來的.這里有多個(gè)size_t
類璃搜,斷點(diǎn)調(diào)試看了下的size
是我們傳進(jìn)來的40郁油,而slot_bytes
剛好是我們的目標(biāo)48蔫仙,那我們就來看下40->48是怎么來的,將error
的異常判斷分支折疊起來,查看主流程:
- 其中
segregated_next_block
就是指針內(nèi)存開辟算法类茂,目的是找到合適的內(nèi)存并返回(不斷遞歸去尋找合適的內(nèi)存空間) -
slot_bytes
是加密算法的鹽(其目的是為了讓加密算法更加安全冗疮,本質(zhì)就是一串自定義的數(shù)字)
⑦ 16字節(jié)對齊
分析:size 是 40够坐,在經(jīng)過 (40 + 16 - 1) >> 4 << 4 操作后寸宵,結(jié)果為48崖面,也就是16的整數(shù)倍——即16字節(jié)對齊.
寫在后面
總結(jié):
- 對象的屬性是按照8字節(jié)進(jìn)行對齊的
- 對象本身則是按照16字節(jié)進(jìn)行對齊的
- 因?yàn)閮?nèi)存是連續(xù)的,通過 16 字節(jié)對齊規(guī)避了風(fēng)險(xiǎn)和容錯(cuò)梯影,有效的防止了訪問溢出
- 同時(shí)巫员,也提高了尋址訪問效率,也就是通常我們所說的空間換時(shí)間
- 和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.