三顧 alloc 之旅

1. 寫在前面

首先看個 🌰objective-c

對象地址打印

從圖中咱們能夠看出,三個對象的地址都不相同,由於這是聲明的三個不一樣的對象,可是值都是指向的同一個地址,而是因爲他們都是經過 obj1 這個對象賦值,指向的都是同一個對象,而這個對象是經過 alloc 方法建立的,並且 obj2 調用了 init方法以後仍是相同的值,說明這個對象就是被alloc方法建立而後返回的。算法

NSObject 對於 iOSer 來講或許已經熟悉得不能再熟悉,開發中大部分的對象都是繼承子它,建立的對象都是基於 NSObject 類,同時也提供了不少方法;萬物皆有出處,深刻思考後咱們可能會疑惑到底這個對象是怎麼生成的,底層作了什麼處理,有哪些不同的設計思想和實現方式;所以讓咱們一塊兒開啓三顧 alloc 旅程編程

2. 探索姿式

要探險首先也得作好攻略,準備生存裝備和技能;那麼探索源碼,也須要正確的知(姿)識(勢),纔不會致使摸不到方向,無功而返,這裏介紹三種姿式供你們入門 ^_^ 。api

普通斷點設置 + 跟蹤調試

普通斷點

按住control + in 進入調試查看彙編調用信息;這裏沒有覺得是模擬器環境,因此沒有跟蹤到objc_alloc的相關信息;使用真機狀況下能看到定位到 libobjc.A.dylib objc_alloc,因此是要探究的方法在 libobjc.A.dylib 這個庫中。sass

符號斷點設置

符號斷點

要探究alloc方法,因此咱們添加一個alloc的符號斷點,這時候會顯示斷到不少類的alloc方法,可是沒有關係,由於咱們斷點了代碼所在對象,因此咱們直接調試就行;斷點執行下一步,就能看到 libobjc.A.dylib + [NSObject alloc],說明是調用了libobjc.A.dylib庫中的NSObjectalloc 類方法。安全

彙編跟蹤

開啓匯跟蹤設置
Debug => Debug Workflow => Always Show Disassembly 開啓匯跟蹤設置。

彙編信息
可查看到代碼 [MRObject alloc]的彙編代碼下一步是調用 objc_alloc; 因此咱們須要研究 objc_alloc方法即爲 alloc的底層實現方法!

objc 源碼

蘋果開源源碼地址 => opensource.apple.com/tarballs/多線程

搜索 objc 便可看到 objc 源碼地址 當前 objc4-756.2 爲最新版本!架構

objc源碼版本列表

ps: 在objc源碼中咱們看到不少 oldnew 的文件命名標識,這表明objc在迭代過程當中過渡了兩個版本,一個新版,一箇舊版,如今咱們都是使用的新版本api,因此查看源碼邏輯的時候只須要定位 objc2 或者 new標識的新版本~app

源碼調試準備條件

經過查看objc源碼和彙編的指令查看 objc_alloc 的流程發現很難定位跟蹤,不能直觀的跟蹤到具體的調用方法路徑;因此將objc源碼集成到咱們的調試工程中,經過打斷點調試跟蹤才能一目瞭然,暢通無阻,所向披靡...以上一切都是理想YY,hahaha... ^_^;如何集成objc源碼,移步 => iOS_objc4-756.2 最新源碼編譯調試post

流程調試

alloc => objc_rootAlloc

第一層調用

callAlloc

callAlloc方法

首先看 __OBJC2__的分支,如今是使用的新版本;而後看第一個判斷fastpath(!cls->ISA()->hasCustomAWZ()),這裏是判斷該類是否有重寫initWithZone;第二層判斷 fastpath(cls->canAllocFast()),是否能夠快速建立,這裏會返回false,具體實現見下分析。

canAllocFast

調用 bits.canAllocFast方法

bool canAllocFast() {
        assert(!isFuture());
        return bits.canAllocFast();
    }
複製代碼

跟蹤 canAllocFast 實現

bits.canAllocFast

查看 FAST_ALLOC 宏定義

FAST_ALLOC

class_createInstance

class_createInstance

打印obj的值,印證流程

打印

申請內存之字節對齊

什麼是字節對齊

現代計算機中,內存空間按照字節劃分,理論上能夠從任何起始地址訪問任意類型的變量。但實際中在訪問特定類型變量時常常在特定的內存地址訪問,這就須要各類類型數據按照必定的規則在空間上排列,而不是順序一個接一個地存放,這就是字節對齊。

對齊的緣由和做用

不一樣硬件平臺對存儲空間的處理上存在很大的不一樣。某些平臺對特定類型的數據只能從特定地址開始存取,而不容許其在內存中任意存放。例如Motorola 68000 處理器不容許16位的字存放在奇地址,不然會觸發異常,所以在這種架構下編程必須保證字節對齊。

但最多見的狀況是,若是不按照平臺要求對數據存放進行對齊,會帶來存取效率上的損失。好比32位的Intel處理器經過總線訪問(包括讀和寫)內存數據。每一個總線週期從偶地址開始訪問32位內存數據,內存數據以字節爲單位存放。若是一個32位的數據沒有存放在4字節整除的內存地址處,那麼處理器就須要2個總線週期對其進行訪問,顯然訪問效率降低不少。

所以,經過合理的內存對齊能夠提升訪問效率。爲使CPU可以對數據進行快速訪問,數據的起始地址應具備「對齊」特性。好比4字節數據的起始地址應位於4字節邊界上,即起始地址可以被4整除。

此外,合理利用字節對齊還能夠有效地節省存儲空間。但要注意,在32位機中使用1字節或2字節對齊,反而會下降變量訪問速度。所以須要考慮處理器類型。還應考慮編譯器的類型。在VC/C++和GNU GCC中都是默認是4字節對齊。

對齊原則

1.數據類型自身的對齊值:char型數據自身對齊值爲1字節,short型數據爲2字節,int/float型爲4字節,double型爲8字節。

2.結構體或類的自身對齊值:其成員中自身對齊值最大的那個值。

3.指定對齊值:#pragma pack (value)時的指定對齊值value。

4.數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小者,即有效對齊值=min{自身對齊值,當前指定的pack值}。

instanceSize 獲取實例對象內存大小

size_t instanceSize(size_t extraBytes) {
  	// 對齊的大小 + extraBytes(額外加的字節數)
  	size_t size = alignedInstanceSize() + extraBytes;
  	// CF requires all objects be at least 16 bytes.
  	// 這裏選擇最小16,由於已經至少8字節了,爲了防止在讀取或者存儲的時候多線程越界之類的安全性;16處於一個基本的合理節約的大小,cup讀取的時候比較合理
  	if (size < 16) size = 16;
  	return size;
}
複製代碼

字節對齊

// alignedInstanceSize
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
  	// 調用 unalignedInstanceSize 獲取內存字節大小,而後調用 word_align 方法對齊
  	return word_align(unalignedInstanceSize());
}

#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL // 7
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif

// 對齊算法 (8字節對齊)
// ps: &與運算,同爲1則爲1,否爲0;|或運算,一個爲1則爲1,否爲0;^異或運算,兩個不相同則爲1,否爲0;~非運算,取反
static inline uint32_t word_align(uint32_t x) {
  	// WORD_MASK = 7,先非運算,而後與運算,二進制
    // 例如這裏是 x = 8; 由於 MRObject 類結構咱們沒有聲明其餘屬性之類的,只有默認包含的 isa 屬性結構,佔用8字節
  
  	// x+mask = 8 + 7 = 15 => 0000 1111
  
  	// mask => 0000 0111
  	// ~7 非運算,取反
  	// a => 1111 1000
  	// & 與運算,同時爲1則爲1
  	// x+mask => 0000 1111
  	// return 8 => 0000 1000
  	// 本質就是 mask = 7 操做 => 獲得8的倍數,8字節對齊,以8爲倍數;至關於 x >> 3 << 3, 右移三位,左移三位 (枚舉定義也是如此,計算快速)
    return (x + WORD_MASK) & ~WORD_MASK;
}

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
  	assert(isRealized());
  	// 對象的實例內存字節大小,是根據類的ivarList、methodList、propertyList、protocolList 等相關的屬性相加計算出來的
  	// 這些數據存儲在類結構中的 data 段的 ro 結構中,這裏經過 ro 獲取具體須要建立的字節數大小
  	return data()->ro->instanceSize;
}
複製代碼

可能會疑惑爲何須要8字節對齊,(⊙o⊙)…???

cpu讀取的時候不知道連續的內存開闢的大小,訪問的時候可能一個1字節,4字節,8字節...,而後讀取的時候須要先判斷多長而後在讀取,比較麻煩;所以cpu就想着若是每一段都是8字節那麼讀取的時候就比較方便,空間換取時間

印證字節對齊

怎麼印證本身對齊呢,那麼咱們經過LLDB調試打印對象內存結構,查看信息來驗證。

首先類聲明幾個屬性:

@property (nonatomic, copy) NSString *name;     // 8個字節
@property (nonatomic, assign) int age;          // 4個字節
@property (nonatomic, assign) long height;      // 8個字節
@property (nonatomic, copy) NSString *hobby;    // 8個字節
複製代碼

內存打印

😤說好的能所有打出呢,嗯哼?爲啥沒能訪問到hobby呢?這是由於咱們只打印了四段內存地址,而加上isa 一共是默認5個屬性,因此hobby在第五段內存,下面是打印日誌信息😺。

(lldb) x/5xg obj1
0x1010a1f40: 0x001d800100001521 0x0000000000000012
0x1010a1f50: 0x00000001000010a8 0x00000000000000b9
0x1010a1f60: 0x00000001000010c8
(lldb) po 0x00000001000010c8
money
複製代碼

alloc 流程圖

流程圖

結束語

以上爲簡要分析alloc流程,有歧義歡迎指出,持續更新進階之旅,未完待續。。。

相關文章
相關標籤/搜索