因為Objective-C的 runtime 只能在 Mac OS 下才能編譯,所以文章中的代碼都是在 Mac OS,也就是 x86_64 架構下運行的,對於在 arm64 中運行的代碼會特別說明。
如果你曾經對Objective-C底層的實現有一定的了解,你應該會知道 Objective-C 對象都是 C 語言結構體,所有的對象都包含一個類型為 isa 的指針,那麼你可能確實對Objective-C的底層有所知,不過現在的Objective-C對象的結構已經不是這樣了。代替 isa 指針的是結構體 isa_t, 這個結構體中"包含"了當前對象指向的類的信息,這篇文章中會介紹一些關於這個變化的知識。
struct objc_object { isa_t isa; };
當 ObjC 為為一個對象分配內存,初始化實例變量後,在這些對象的實例變量的結構體中的第一個就是 isa。
所有繼承自 NSObject 的類實例化後的對象都會包含一個類型為 isa_t 的結構體。
從上圖中可以看出,不只是實例會包含一個 isa 結構體,所有的類也有這麼一個 isa。在 ObjC 中 Class 的定義也是一個名為 objc_class 的結構體,如下:
struct objc_class : objc_object { isa_t isa; Class superclass; cache_t cache; class_data_bits_t bits; };
由於 objc_class 結構體是繼承自 objc_object 的,所以在這裡顯式地寫出了 isa_t isa 這個成員變量。
isa 指針的作用與元類
到這裡,我們就明白了:Objective-C 中類也是一個對象。
這個 isa 包含了什麼呢?回答這個問題之前,要引入了另一個概念 元類(meta class),我們先了解一些關於元類的信息。
因為在 Objective-C 中,對象的方法並沒有存儲於對象的結構體中(如果每一個對象都保存了自己能執行的方法,那麼對內存的占用有極大的影響)。
當實例方法被調用時,它要通過自己持有的 isa 來查找對應的類,然後在這裡的 class_data_bits_t 結構體中查找對應方法的實現。同時,每一個 objc_class 也有一個指向自己的父類的指針 super_class 用來查找繼承的方法。
關於如何在 class_data_bits_t 中查找對應方法會在之後的文章中講到。這裡只需要知道,它會在這個結構體中查找到對應方法的實現就可以了。
但是,這樣就有一個問題,類方法的實現又是如何查找並且調用的呢?這時,就需要引入元類來保證無論是類還是對象都能通過相同的機制查找方法的實現。
讓每一個類的 isa 指向對應的元類,這樣就達到了使類方法和實例方法的調用機制相同的目的:
實例方法調用時,通過對象的 isa 在類中獲取方法的實現
類方法調用時,通過類的 isa 在元類中獲取方法的實現
下面這張圖介紹了對象,類與元類之間的關系,筆者認為已經覺得足夠清晰了,所以不在贅述。
有關與介紹類與元類之間的關系的文章實在是太多了,因為這篇文章主要介紹 isa,在這一小節只是對其作用以及元類的概念進行介紹。如果想要了解更多關於類與元類的信息,可以看 What is a meta-class in Objective-C?
結構體 isa_t
其實 isa_t 是一個定義得非常"奇怪"的結構體,在 ObjC 源代碼中可以看到這樣的定義:
#define ISA_MASK 0x00007ffffffffff8ULL #define ISA_MAGIC_MASK 0x001f800000000001ULL #define ISA_MAGIC_VALUE 0x001d800000000001ULL #define RC_ONE (1ULL<<56) #define RC_HALF (1ULL<<7) union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; struct { uintptr_t indexed : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 44; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 8; }; };
這是在 __x86_64__ 上的實現,對於 iPhone5s 等架構為 __arm64__ 的設備上,具體結構體的實現和位數可能有些差別,不過這些字段都是存在的,可以看這裡的 arm64 上結構體的實現。
在本篇文章中, 我們會以 __x86_64__ 為例進行分析,而不會對兩種架構下由於不同的內存布局方式導致的差異進行分析。在我看來,這個細節不會影響對 isa 指針的理解,不過還是要知道的。
筆者對這個 isa_t 的實現聲明順序有一些更改,更方便分析和理解。
union isa_t { ... };
isa_t 是一個 union 類型的結構體,對 union 不熟悉的讀者可以看這個 stackoverflow 上的回答。也就是說其中的 isa_t、cls、 bits 還有結構體共用同一塊地址空間。而 isa 總共會占據 64 位的內存空間(決定於其中的結構體)
struct { uintptr_t indexed : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 44; 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 的初始化
我們可以通過 isa 初始化的方法 initIsa 來初步了解這 64 位的 bits 的作用:
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { initIsa(cls, true, hasCxxDtor); } inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { if (!indexed) { isa.cls = cls; } else { isa.bits = ISA_MAGIC_VALUE; isa.has_cxx_dtor = hasCxxDtor; isa.shiftcls = (uintptr_t)cls >> 3; } }
indexed 和 magic
當我們對一個 ObjC 對象分配內存時,其方法調用棧中包含了上述的兩個方法,這裡關注的重點是 initIsa 方法,由於在 initInstanceIsa 方法中傳入了 indexed = true,所以,我們簡化一下這個方法的實現:
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { isa.bits = ISA_MAGIC_VALUE; isa.has_cxx_dtor = hasCxxDtor; isa.shiftcls = (uintptr_t)cls >> 3; }
對整個 isa 的值 bits 進行設置,傳入 ISA_MAGIC_VALUE:
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
我們可以把它轉換成二進制的數據,然後看一下哪些屬性對應的位被這行代碼初始化了(標記為紅色):
從圖中了解到,在使用 ISA_MAGIC_VALUE 設置 isa_t 結構體之後,實際上只是設置了 indexed 以及 magic 這兩部分的值。
其中 indexed 表示 isa_t 的類型
0 表示 raw isa,也就是沒有結構體的部分,訪問對象的 isa 會直接返回一個指向 cls 的指針,也就是在 iPhone 遷移到 64 位系統之前時 isa 的類型。
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; };
1 表示當前 isa 不是指針,但是其中也有 cls 的信息,只是其中關於類的指針都是保存在 shiftcls 中。
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; struct { uintptr_t indexed : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 44; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 8; }; };
magic 的值為 0x3b 用於調試器判斷當前對象是真的對象還是沒有初始化的空間
has_cxx_dtor
在設置 indexed 和 magic 值之後,會設置 isa 的 has_cxx_dtor,這一位表示當前對象有 C++ 或者 ObjC 的析構器(destructor),如果沒有析構器就會快速釋放內存。
isa.has_cxx_dtor = hasCxxDtor;
shiftcls
在為 indexed、 magic 和 has_cxx_dtor 設置之後,我們就要將當前對象對應的類指針存入 isa 結構體中了。
isa.shiftcls = (uintptr_t)cls >> 3;
將當前地址右移三位的主要原因是用於將 Class 指針中無用的後三位清楚減小內存的消耗,因為類的指針要按照字節(8 bits)對齊內存,其指針後三位都是沒有意義的 0。
絕大多數機器的架構都是 byte-addressable 的,但是對象的內存地址必須對齊到字節的倍數,這樣可以提高代碼運行的性能,在 iPhone5s 中虛擬地址為 33 位,所以用於對齊的最後三位比特為 000,我們只會用其中的 30 位來表示對象的地址。
而 ObjC 中的類指針的地址後三位也為 0,在 _class_createInstanceFromZone 方法中打印了調用這個方法傳入的類指針:
可以看到,這裡打印出來的所有類指針十六進制地址的最後一位都為 8 或者 0。也就是說,類指針的後三位都為 0,所以,我們在上面存儲 Class 指針時右移三位是沒有問題的。
isa.shiftcls = (uintptr_t)cls >> 3;
如果再嘗試打印對象指針的話,會發現所有對象內存地址的後四位都是 0,說明 ObjC 在初始化內存時是以 16 個字節對齊的, 分配的內存地址後四位都是 0。
使用整個指針大小的內存來存儲 isa 指針有些浪費,尤其在 64 位的 CPU 上。在 ARM64 運行的 iOS 只使用了 33 位作為指針(與結構體中的 33 位無關,Mac OS 上為 47 位),而剩下的 31 位用於其它目的。類的指針也同樣根據字節對齊了,每一個類指針的地址都能夠被 8 整除,也就是使最後 3 bits 為 0,為 isa 留下 34 位用於性能的優化。
Using an entire pointer-sized piece of memory for the isa pointer is a bit wasteful, especially on 64-bit CPUs which don't use all 64 bits of a pointer. ARM64 running iOS currently uses only 33 bits of a pointer, leaving 31 bits for other purposes. Class pointers are also aligned, meaning that a class pointer is guaranteed to be divisible by 8, which frees up another three bits, leaving 34 bits of the isa available for other uses. Apple's ARM64 runtime takes advantage of this for some great performance improvements. from ARM64 and You
我嘗試運行了下面的代碼將 NSObject 的類指針和對象的 isa 打印出來,具體分析一下
object_pointer: 0000000001011101100000000000000100000000001110101110000011111001 // 補全至 64 位
class_pointer: 100000000001110101110000011111000
編譯器對直接訪問 isa 的操作會有警告,因為直接訪問 isa 已經不會返回類指針了,這種行為已經被棄用了,取而代之的是使用 ISA() 方法來獲取類指針。
代碼中的 object 對象的 isa 結構體中的內容是這樣的:
其中紅色的為類指針,與上面打印出的 [NSObject class] 指針右移三位的結果完全相同。這也就驗證了我們之前對於初始化 isa 時對 initIsa 方法的分析是正確的。它設置了 indexed、magic 以及 shiftcls。
ISA() 方法
因為我們使用結構體取代了原有的 isa 指針,所以要提供一個方法 ISA() 來返回類指針。
其中 ISA_MASK 是宏定義,這裡通過掩碼的方式獲取類指針:
#define ISA_MASK 0x00007ffffffffff8ULL inline Class objc_object::ISA() { return (Class)(isa.bits & ISA_MASK); }
其它 bits
在 isa_t 中,我們還有一些沒有介紹的其它 bits,在這個小結就簡單介紹下這些 bits 的作用
has_assoc:對象含有或者曾經含有關聯引用,沒有關聯引用的可以更快地釋放內存
weakly_referenced:對象被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放
deallocating:對象正在釋放內存
has_sidetable_rc:對象的引用計數太大了,存不下
extra_rc:對象的引用計數超過 1,會存在這個這個裡面,如果引用計數為 10,extra_rc 的值就為 9
struct { uintptr_t indexed : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 44; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 8; };
arm64 架構中的 isa_t 結構體
#define ISA_MASK 0x0000000ffffffff8ULL #define ISA_MAGIC_MASK 0x000003f000000001ULL #define ISA_MAGIC_VALUE 0x000001a000000001ULL #define RC_ONE (1ULL<<45) #define RC_HALF (1ULL<<18) union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; struct { uintptr_t indexed : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 33; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19; }; };
參考資料
Objective-C Runtime Programming Guide
What is a meta-class in Objective-C?
objc_explain_Classes_and_metaclasses
Storing things in isa
Why do we need C Unions
objc_explain_Non-pointer_isa
Tagged Pointer
ARM64 and You
64位與Tagged Pointer