iOS 底層探索對象篇之 calloc 和 isa

上一篇文章主要咱們探索了 iOS  對象的 alloc 和 init 以及對象是怎麼開闢內存以及初始化的,若是在對象身上增長一些屬性,是否會影響內存開闢呢?還有一個遺留問題就是經過 calloc ,咱們的對象有了內存地址,可是對象結構裏面的 isa 是怎麼關聯到咱們的對象的內存地址的呢。objective-c

1、calloc 底層探索

在探索 calloc 底層前,咱們先補充一下內存對齊相關的知識點。算法

1.1 內存對齊三原則

iOS 中,對象的屬性須要進行內存對齊,而對象自己也須要進行內存對齊。
內存對齊有三原則bash

  • 數據成員對齊原則: 結構( struct )(或聯合( union ))的數據成員,第
    一個數據成員放在 offset 爲 0 的地方,之後每一個數據成員存儲的起始位置要
    從該成員大小或者成員的子成員大小
  • 結構體做爲成員: 若是一個結構裏有某些結構體成員,則結構體成員要從
    其內部最大元素大小的整數倍地址開始存儲
  • 收尾工做: 結構體的總大小,也就是 sizeof 的結果,.必須是其內部最大
    成員的整數倍.不足的要補⻬。
    翻譯一下就是:
  • 前面的地址必須是後面的地址正數倍,不是就補齊
  • 結構體裏面的嵌套結構體大小要以該嵌套結構體最大元素大小的整數倍
  • **整個 ****Struct** 的地址必須是最大字節的整數倍

1.2 對象申請內存和系統開闢內存

咱們經過打印下面的代碼:架構

NSLog(@"%lu - %lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
複製代碼

能夠發現對象本身申請的內存大小與系統實際給咱們開闢的大小時不同的,這裏對象申請的內存大小是 40 個字節,而系統開闢的是 48 個字節。ide

40 個字節不難理解,是由於當前對象有 4 個屬性,有三個屬性爲 8 個字節,有一個屬性爲 4個字節,再加上 isa 的 8 個字節,就是 32 + 4 = 36 個字節,而後根據內存對齊原則,36 不能被 8 整除,36 日後移動恰好到了 40 就是 8 的倍數,因此內存大小爲 40。函數

48 個字節的話須要咱們探索 calloc 的底層原理。測試

這裏還有一個注意點,就是 class_getInstanceSizemalloc_size 對同一個對象返回的結果不同的,緣由是 malloc_size 是直接返回的 calloc 以後的指針的大小,回憶上一節課,這裏有一步在調用 calloc 以前的操做以下:優化

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
複製代碼

class_getInstanceSize 內部實現是:ui

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
複製代碼

也就是說 class_getInstanceSize 會輸出 8 個字節,malloc_size 會輸出 16 個字節,固然前提是該對象沒有任何屬性。this

1.3 探索 calloc 底層

咱們從 calloc 函數出發,可是咱們直接在 libObjc 的源碼中是找不到其對應實現的,經過觀察 Xcode 咱們知道其實應該找 libMalloc 源碼纔對:

這裏有個小技巧,其實咱們研究的是 calloc 的底層原理,而 libObjclibMalloc 是相互獨立的,因此在 libMalloc 源碼裏面,咱們不必去走 calloc 前面的流程了。咱們經過斷點調試 libObjc 源碼能夠知道第二個參數是 40: (這是由於當前發送 alloc 消息的對象有 4 個屬性,每一個屬性 8 個字節,再加上 isa 的 8 個字節,因此就是 40 個字節)

接下來咱們打開 libMalloc 的源碼,在新建的 target 中直接手動聲明以下的代碼:

void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
複製代碼

Command + Run 以後咱們會看到報錯信息:

這個時候咱們會使用搜索大法,直接 Command + Shift + F 進行全局搜索對應的符號,可是會發現找不到,咱們再仔細觀察,這些符號都是位於 .o 文件裏面的,因此咱們能夠去掉符號前面的下劃線再進行搜索,這個時候就能夠把對應的代碼註釋而後從新運行了。

運行以後咱們一直沿着源碼斷點下去,會來到這麼一段代碼

ptr = zone->calloc(zone, num_items, size);
複製代碼

咱們若是直接去找 calloc,就會遞歸了,因此咱們須要點進去,而後咱們會發現一個很複雜的東西出現了:

這裏咱們能夠直接在斷點處使用 LLDB 命令打印這行代碼來看具體實現是位於哪一個文件中

p zone->calloc
輸出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001003839c7 (.dylib`default_zone_calloc at malloc.c:249)
複製代碼

也就是說 zone->alloc 的真正實現是在 malloc.c 源文件的249行處。

static void * default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) {
	zone = runtime_default_zone();
	
	return zone->calloc(zone, num_items, size);
}
複製代碼

可是咱們發現這裏又是一次 zone->calloc,咱們接着再次使用 LLDB 打印內存地址:

p zone->calloc
輸出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x0000000100384faa (.dylib`nano_calloc at nano_malloc.c:884)
複製代碼

咱們再次來到 nano_calloc 方法

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
	size_t total_bytes;

	if (calloc_get_size(num_items, size, 0, &total_bytes)) {
		return NULL;
	}

	if (total_bytes <= NANO_MAX_SIZE) {
		void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}
	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->calloc(zone, 1, total_bytes);
}
複製代碼

咱們簡單分析一下,應該往 _nano_malloc_check_clear 裏面繼續走,而後咱們發現 _nano_malloc_check_clear 裏面內容很是多,這個時候咱們要明確一點,咱們的目的是找出 48 是怎麼算出來的,通過分析以後,咱們來到 segregated_size_to_fit

static MALLOC_INLINE size_t segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey) {
	// size = 40
	size_t k, slot_bytes;

	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	// 40 + 16-1 >> 4 << 4
	// 40 - 16*3 = 48

	//
	// 16
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!

	return slot_bytes;
}
複製代碼

這裏能夠看出進行的是 16 字節對齊,那麼也就是說咱們傳入的 size 是 40,在通過 (40 + 16 - 1) >> 4 << 4 操做後,結果爲48,也就是16的整數倍。

總結:

  • 對象的屬性是進行的 8 字節對齊
  • 對象本身進行的是 16 字節對齊
    • 由於內存是連續的,經過 16 字節對齊規避風險和容錯,防止訪問溢出
    • 同時,也提升了尋址訪問效率,也就是空間換時間

2、isa 底層探索

2.1 聯合體位域

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
};
複製代碼

咱們探索 isa 的時候,會發現 isa 實際上是一個聯合體,而這實際上是從內存管理層面來設計的,由於聯合體是全部成員共享一個內存,聯合體內存的大小取決於內部成員內存大小最大的那個元素,對於 isa 指針來講,就不用額外聲明不少的屬性,直接在內部的 ISA_BITFIELD 保存信息。同時因爲聯合體屬性間是互斥的,因此 cls 和 bits 在 isa 初始化流程時是在兩個分支中被賦值的。

image.png

2.2 isa 結構

isa 做爲一個聯合體,有一個結構體屬性爲 ISA_BITFIELD,其大小爲 8 個字節,也就是 64 位。
下面的代碼是基於 arm64 架構的:

# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19
複製代碼
  • nonpointer: 表示是否對 isa 指針開啓指針優化
    • 0: 純 isa 指針
    • 1: 不止是類對象地址, isa 中包含了類信息、對象的引用計數等
  • has_assoc: 關聯對象標誌位,0 沒有,1 存在
  • has_cxx_dtor: 該對象是否有 C++ 或者 Objc 的析構器,若是有析構函數,則須要作析構邏輯, 若是沒有,則能夠更快的釋放對象
  • shiftcls: 存儲類指針的值。開啓指針優化的狀況下,在 arm64 架構中有 33 位用來存儲類指針。
  • magic: 用於調試器判斷當前對象是真的對象仍是沒有初始化的空間
  • weakly_referenced: 標誌對象是否被指向或者曾經指向一個 ARC 的弱變量,
    沒有弱引用的對象能夠更快釋放。
  • deallocating: 標誌對象是否正在釋放內存
  • has_sidetable_rc: 當對象引用技術大於 10 時,則須要借用該變量存儲進位
  • extra_rc: 當表示該對象的引用計數值,其實是引用計數值減 1, 例如,若是對象的引用計數爲 10,那麼 extra_rc 爲 9。若是引用計數大於 10, 則須要使用到下面的 has_sidetable_rc。

2.3 isa 關聯對象和類

isa 是對象中的第一個屬性,由於這一步是在繼承的時候發生的,要早於對象的成員變量,屬性列表,方法列表以及所遵循的協議列表。

咱們在探索 alloc 底層原理的時候,有一個方法叫作 initIsa

這個方法的做用就是初始化 isa 聯合體位域。其中有這麼一行代碼:

newisa.shiftcls = (uintptr_t)cls >> 3;
複製代碼

經過這行代碼,咱們知道 shiftcls 這個位域其實存儲的是類的信息。這個類就是實例化對象所指向的那個類。

經過 LLDB 進行調試打印,咱們能夠知道一個對象的 isa 會關聯到這個對象所屬的類。

這裏的左移右移操做其實很好理解,首先咱們先觀察 isaISA_BITFIELD 位域的結構:

// 注:這裏是x64架構
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 8
複製代碼

咱們能夠看到,ISA_BITFIELD 的前 3 位是 nonpointerhas_assochas_cxx_dtor,中間 44 位是 shiftcls ,後面 17 位是剩餘的內容,同時由於 iOS 是小端模式,那麼咱們就須要去掉右邊的 3 位和左邊的 17位,因此就會採用 >>3<<3 而後 <<17>>17 的操做了。

經過這個測試,咱們就知道了 isa 實現了對象與類之間的關聯。

咱們還能夠探索 object_getClass 底層,能夠發現有這樣一行代碼:

return (Class)(isa.bits & ISA_MASK);
複製代碼

這行代碼就是將 isa 中的聯合體位域與上一個蒙版,這個蒙版定義是怎麼樣的呢?

# define ISA_MASK 0x00007ffffffffff8ULL
複製代碼

0x00007ffffffffff8ULL 這個值咱們轉成二進制表示:

0000 0000 0000 0000 0111 1111 1111 1111 
1111 1111 1111 1111 1111 1111 1111 1000
複製代碼

結果一目瞭然,這個蒙版就是幫咱們去過濾掉除 shiftcls 以外的內容。

咱們直接將對象的 isa 地址與上這個mask以後,就會獲得 object.class 同樣的內存地址。

2.4 isa 走位分析

2.4.1 類與元類

咱們都知道對象能夠建立多個,可是類是否能夠建立多個呢?
答案很簡單,一個。那麼若是來驗證呢?

//MARK: - 分析類對象內存存在個數
void lgTestClassNum(){
    Class class1 = [LGPerson class];
    Class class2 = [LGPerson alloc].class;
    Class class3 = object_getClass([LGPerson alloc]);
    Class class4 = [LGPerson alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

// 打印輸出以下:

0x100002108-
0x100002108-
0x100002108-
0x100002108
複製代碼

因此咱們就知道了類在內存中只會存在一份。

(lldb) x/4gx LGTeacher.class
0x100001420: 0x001d8001000013f9 0x0000000100b38140
0x100001430: 0x00000001003db270 0x0000000000000000
(lldb) po 0x001d8001000013f9
17082823967917874

(lldb) p 0x001d8001000013f9
(long) $2 = 8303516107936761
(lldb) po 0x100001420
LGTeacher
複製代碼

咱們經過上面的打印,就發現 類的內存結構裏面的第一個結構打印出來仍是 LGTeacher,那麼是否是就意味着 對象->類->類 這樣的死循環呢?這裏的第二個類實際上是 元類。是由系統幫咱們建立的。這個元類也沒法被咱們實例化。

也就是下面的這種關係:

(lldb) p/x 0x001d8001000013f9 & 0x00007ffffffffff8
(long) $4 = 0x00000001000013f8
(lldb) po 0x00000001000013f8
LGTeacher

(lldb) x/4gx 0x00000001000013f8
0x1000013f8: 0x001d800100b380f1 0x0000000100b380f0
0x100001408: 0x0000000101c30230 0x0000000100000007
(lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8
(long) $6 = 0x0000000100b380f0
(lldb) po 0x0000000100b380f0
NSObject
複製代碼

2.4.2 isa 走位

咱們在 Xcode 中測試有如下結果:

由此能夠給出官方的經典 isa 走位圖

2.5 isa 初始化流程圖

image.png

3、對象的本質

在咱們認知裏面,OC 對象的本質就是一個結構體,這個結論在 libObjc 源碼的 objc-private.h 源文件中能夠獲得證明。

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    ...省略其餘的內容...
}
複製代碼

而對於對象所屬的類來講,咱們也能夠在 objc-runtime-new.h 源文件中找到

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    ...省略其餘的內容...
    
}
複製代碼

也就是說 objc_class 內存中第一個位置是 isa,第二個位置是 superclass

不過咱們本着求真的態度能夠用 clang 來重寫咱們的 OC 源文件來查看是否是這麼回事。

clang -rewrite-objc main.m -o main.cpp
複製代碼

這行命令會把咱們的 main.m 文件編譯成 C++ 格式,輸出爲 main.cpp

image.png

咱們能夠看到 LGPerson 對象在底層實際上是一個結構體 objc_object 。

image.png

而咱們的 Class 在底層也是一個結構體 objc_class 。

4、總結

至此, iOS 底層探索之對象篇更新完畢,如今來回顧一下咱們所探索的內容。

  • alloc & init 流程剖析
  • 內存開闢
  • 字節對齊算法
  • isa 初始化和走位
  • 對象的本質

下一篇章咱們要探索篇章的是類,敬請期待~

相關文章
相關標籤/搜索