iOS之武功秘籍②:OC對象原理-中(內(nèi)存對齊和malloc源碼分析)

iOS之武功秘籍 文章匯總

寫在前面

iOS之武功秘籍①:OC對象原理-上(alloc & init & new)一文中講了底層對象創(chuàng)建的流程,那么本文將來探索下對象中的屬性在內(nèi)存中的排列 -- 內(nèi)存對齊 和 malloc源碼分析

本節(jié)可能用到的秘籍Demo

一、對象開辟內(nèi)存的影響因素(補(bǔ)充)

通過上一篇文章,我們已經(jīng)知道,創(chuàng)建一個對象,在經(jīng)過_class_createInstanceFromZone方法時,其內(nèi)部的size = cls->instanceSize(extraBytes)能計(jì)算出創(chuàng)建這個對象所需的內(nèi)存空間大小.那么影響對象開辟內(nèi)存的因素是什么呢?

① 對于對象來說影響其內(nèi)存開辟的因素(影響對象字節(jié)對齊的因素) -- 對象的屬性(更確切的說應(yīng)該是對象的成員變量)

舉個??:
  • 當(dāng)我們的TCJPerson對象沒有其他屬性的時候,只有一個從父類NSObject繼承過來的isa時,此時創(chuàng)建TCJPerson對象所需的開辟的內(nèi)存空間大小為16字節(jié).

  • 當(dāng)我們增加一個name屬性時,此時的size 大小還是 16( if (size < 16) size = 16).


  • 接著我們在增加一個nickName屬性,此時需要的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ù)備知識:
  • pop: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個16進(jìn)制的8字節(jié)地址空間(x表示16進(jìn)制,8表示8個,g表示8字節(jié)為單位,等同于x/8xg 對象

根據(jù)我們的計(jì)算機(jī)基礎(chǔ)和LLDB指令,可以發(fā)現(xiàn)

  • 第一段是isa(從64位開始,isa需要進(jìn)行一個位運(yùn)算 & ISA_MASK 操作,而x86環(huán)境下,ISA_MASK的值為0x0000000ffffffff8ULL)
  • 第二段中0x0000001218 -- 對應(yīng)age,而62、61分別是b、aASCII編碼
  • 第三段中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í)時查看內(nèi)存狀況Debug->Debug Workflow->View Memory(shift + Command +M)

一般不推薦用第二種方式.

二、字節(jié)對齊

① sizeof、class_getInstanceSize、malloc_size

  • sizeof():是一個運(yùn)算符,不是函數(shù).傳入數(shù)據(jù)類型,輸出內(nèi)存大小,在編譯時確定.只與數(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ì)就是一個結(jié)構(gòu)體(即 struct objc_object)的指針,所以sizeof(objc)打印的是對象objc的指針大小,我們知道一個指針的內(nèi)存大小是8字節(jié),所以sizeof(objc) 打印是 8.注意:這里的8字節(jié)與isa指針一點(diǎn)關(guān)系都沒有?。?!
    • 對于指針而言,sizeof打印的就是8,因?yàn)橐粋€指針的內(nèi)存大小是8字節(jié).
  • class_getInstanceSize:計(jì)算對象實(shí)際占用的內(nèi)存大小,這個需要依據(jù)類的屬性而變化,如果自定義類沒有自定義屬性,僅僅只是繼承自NSObject,則類的實(shí)例對象實(shí)際占用的內(nèi)存大小是8,遵循8字節(jié)對齊.

  • malloc_size:計(jì)算對象實(shí)際分配的內(nèi)存大小,這個是由系統(tǒng)完成的.可以從上面的打印結(jié)果看出,實(shí)際分配的和實(shí)際占用的內(nèi)存大小并不相等.

② 對象的內(nèi)存對齊

我們知道就對象整體而言,蘋果系統(tǒng)采用16字節(jié)對齊開辟內(nèi)存大小,提高系統(tǒng)存取性能。

那么對于對象內(nèi)部呢?

  • 對象的本質(zhì)是結(jié)構(gòu)體,這個在后續(xù)篇章中我們會詳細(xì)講解.所以研究對象內(nèi)部的內(nèi)存,就是研究結(jié)構(gòu)體的內(nèi)存布局.
  • 內(nèi)存對齊目的:最大程度提高資源利用率.

③ 結(jié)構(gòu)體內(nèi)存對齊

搞個??瞧瞧:

輸出結(jié)果: CJStruct1-24 CJStruct2-16 CJStruct3-32 CJStruct4-24 .

從打印結(jié)果我們可以看出一個問題,兩個結(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ī)則
每個特定平臺上的編譯器都有自己的默認(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), nm 位置開始存儲, 反之繼續(xù)檢查 m+1 能否整除 n, 直到可以整除, 從而就確定了當(dāng)前成員的開始位置.
  • 數(shù)據(jù)成員為結(jié)構(gòu)體:當(dāng)結(jié)構(gòu)體嵌套了結(jié)構(gòu)體時,作為數(shù)據(jù)成員的結(jié)構(gòu)體的自身長度作為外部結(jié)構(gòu)體的最大成員的內(nèi)存大小(即在確定復(fù)合類型成員的偏移位置時則是將復(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個字節(jié),從0開始,此時min(0,8),即 0-7 存儲 a
  • 變量b:占1個字節(jié),從8開始,此時min(8,1),8能整除1,,即 8 存儲 b
  • 變量c:占4個字節(jié),從9開始,此時min(9,4),9不能整除4,繼續(xù)往后移動,直到min(12,4),從12開始即 12-15 存儲 c
  • 變量d:占2個字節(jié),從16開始,此時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個字節(jié),從0開始,此時min(0,8),即 0-7 存儲 b
  • 變量b:占4個字節(jié),從8開始,此時min(8,4),8可以整除4,即 8-11 存儲 c
  • 變量c:占1個字節(jié),從12開始,此時min(12, 1),12可以整除1,即12 存儲 d
  • 變量d:占2個字節(jié),從13開始,此時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個字節(jié),從0開始,此時min(0,4),即 0-3 存儲 e
  • 結(jié)構(gòu)體成員CJStruct1CJStruct1是一個結(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個字節(jié),從0開始,此時min(0,8),即 0-7 存儲 e
  • 結(jié)構(gòu)體成員CJStruct2CJStruct2是一個結(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é)對齊時會浪費(fèi)大量的內(nèi)存空間,所以這里系統(tǒng)會把對象的屬性重新排列,以此來最大化利用我們的內(nèi)存空間

驗(yàn)證①:
CJStruct1CJStruct2而言,他們的成員屬性一樣,只是他們之間的屬性排列位置不同,他們分別占用不同的內(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)存時的屬性順序是:isa(繼承NSObject) ->age(int) -> c2(char) -> c1(char) -> nickName(NSString) -> name(NSString)-> height(long),并且將age 和 c1 及 c2 存放在了一個塊區(qū). 這就是蘋果的內(nèi)存優(yōu)化的體現(xiàn).

⑤小彩蛋

  • 對于對象之間,系統(tǒng)面對的對象太多,系統(tǒng)為了防止容錯,采用的是16字節(jié)對齊的內(nèi)存,給對象留足夠間距,避免越界訪問(所以malloc_size讀取的都是16的倍數(shù))
  • 但為了避免浪費(fèi)太多內(nèi)存空間,系統(tǒng)會在每個對象內(nèi)部進(jìn)行屬性重排,并使用8字節(jié)對齊,使單個對象占用的資源盡可能小.(所以class_getInstanceSize讀取的都是8的倍數(shù))

三、malloc源碼輔助分析

通過上一篇文章,我們已經(jīng)知道,創(chuàng)建一個對象,在經(jīng)過_class_createInstanceFromZone方法時,其內(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)來了?。。∠胍^續(xù)跟進(jìn)源碼,可以通過以下方法:

方法一: —— 分析zone

已知zonemalloc_zone_t類型的,在第二步中retval = malloc_zone_calloc(default_zone, num_items, size);中傳遞的第一個參數(shù)zone又是default_zone,跟蹤進(jìn)去會發(fā)現(xiàn)它是一個靜態(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í)際調(diào)用default_zone_calloc

方法三: —— 按住control + step into,進(jìn)入calloc的源碼

加上圖中的斷點(diǎn),來到斷點(diǎn)后:按住control + step into同樣也會來到:

這其中有兩個非常重要的操作:

  • 創(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)存申請,并且返回一個成熟的指針ptr.

⑤ _nano_malloc_check_clear

分析:這個方法中有三個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,此時此刻看到這么長的一段代碼也不用慌張,if-else只走其一.再仔細(xì)想想,我們是帶著目的來看源碼的——malloc_size中的48是怎么來的.這里有多個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)險和容錯,有效的防止了訪問溢出
    • 同時,也提高了尋址訪問效率,也就是通常我們所說的空間換時間
  • 和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容