上一篇文章主要咱們探索了 iOS
對象的 alloc
和 init
以及對象是怎麼開闢內存以及初始化的,若是在對象身上增長一些屬性,是否會影響內存開闢呢?還有一個遺留問題就是經過 calloc
,咱們的對象有了內存地址,可是對象結構裏面的 isa
是怎麼關聯到咱們的對象的內存地址的呢。objective-c
calloc
底層探索在探索 calloc
底層前,咱們先補充一下內存對齊相關的知識點。算法
在 iOS
中,對象的屬性須要進行內存對齊,而對象自己也須要進行內存對齊。
內存對齊有三原則bash
struct
)(或聯合( union
))的數據成員,第sizeof
的結果,.必須是其內部最大**Struct**
的地址必須是最大字節的整數倍咱們經過打印下面的代碼:架構
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_getInstanceSize
和 malloc_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
咱們從 calloc
函數出發,可是咱們直接在 libObjc
的源碼中是找不到其對應實現的,經過觀察 Xcode 咱們知道其實應該找 libMalloc
源碼纔對:
這裏有個小技巧,其實咱們研究的是 calloc
的底層原理,而 libObjc
和 libMalloc
是相互獨立的,因此在 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的整數倍。
總結:
isa
底層探索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
初始化流程時是在兩個分支中被賦值的。
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
指針開啓指針優化
isa
指針isa
中包含了類信息、對象的引用計數等isa
是對象中的第一個屬性,由於這一步是在繼承的時候發生的,要早於對象的成員變量,屬性列表,方法列表以及所遵循的協議列表。
咱們在探索 alloc
底層原理的時候,有一個方法叫作 initIsa
。
這個方法的做用就是初始化 isa
聯合體位域。其中有這麼一行代碼:
newisa.shiftcls = (uintptr_t)cls >> 3;
複製代碼
經過這行代碼,咱們知道 shiftcls
這個位域其實存儲的是類的信息。這個類就是實例化對象所指向的那個類。
經過 LLDB
進行調試打印,咱們能夠知道一個對象的 isa
會關聯到這個對象所屬的類。
這裏的左移右移操做其實很好理解,首先咱們先觀察 isa
的 ISA_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 位是 nonpointer
,has_assoc
,has_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
同樣的內存地址。
咱們都知道對象能夠建立多個,可是類是否能夠建立多個呢?
答案很簡單,一個。那麼若是來驗證呢?
//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
複製代碼
咱們在 Xcode 中測試有如下結果:
由此能夠給出官方的經典 isa
走位圖
在咱們認知裏面,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
。
咱們能夠看到 LGPerson
對象在底層實際上是一個結構體 objc_object
。
而咱們的 Class
在底層也是一個結構體 objc_class
。
至此, iOS
底層探索之對象篇更新完畢,如今來回顧一下咱們所探索的內容。
下一篇章咱們要探索篇章的是類,敬請期待~