神經病院 Objective-C Runtime 入院第一天—— isa 和 Class
目錄
- 1.Runtime簡介
- 2.NSObject起源
- (1) isa_t結構體的具體實現
- (2) cache_t的具體實現
- (3) class_data_bits_t的具體實現
- 3.入院考試
一. Runtime簡介
Runtime 又叫執行時,是一套底層的 C 語言 API,是 iOS 系統的核心之一。開發者在編碼過程中,可以給任意一個物件傳送訊息,在編譯階段只是確定了要向接收者傳送這條訊息,而接受者將要如何響應和處理這條訊息,那就要看執行時來決定了。
C語言中,在編譯期,函式的呼叫就會決定呼叫哪個函式。
而OC的函式,屬於動態呼叫過程,在編譯期並不能決定真正呼叫哪個函式,只有在真正執行時才會根據函式的名稱找到對應的函式來呼叫。
Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。
Objc 在三種層面上與 Runtime 系統進行互動:
1. 通過 Objective-C 原始碼
一般情況開發者只需要編寫 OC 程式碼即可,Runtime 系統自動在幕後把我們寫的原始碼在編譯階段轉換成執行時程式碼,在執行時確定對應的資料結構和呼叫具體哪個方法。
2. 通過 Foundation 框架的 NSObject 類定義的方法
在OC的世界中,除了NSProxy類以外,所有的類都是NSObject的子類。在Foundation框架下,NSObject和NSProxy兩個基類,定義了類層次結構中該類下方所有類的公共介面和行為。NSProxy是專門用於實現代理物件的類,這個類暫時本篇文章不提。這兩個類都遵循了NSObject協議。在NSObject協議中,聲明瞭所有OC物件的公共方法。
在NSObject協議中,有以下5個方法,是可以從Runtime中獲取資訊,讓物件進行自我檢查。
Objective-C- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
-class方法返回物件的類; -isKindOfClass: 和 -isMemberOfClass: 方法檢查物件是否存在於指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變數); -respondsToSelector: 檢查物件能否響應指定的訊息; -conformsToProtocol:檢查物件是否實現了指定協議類的方法;
在NSObject的類中還定義了一個方法
Objective-C- (IMP)methodForSelector:(SEL)aSelector;
這個方法會返回指定方法實現的地址IMP。
以上這些方法會在本篇文章中詳細分析具體實現。
3. 通過對 Runtime 庫函式的直接呼叫
關於這一點,其實還有一個小插曲。當我們匯入了objc/Runtime.h和objc/message.h兩個標頭檔案之後,我們查詢到了Runtime的函式之後,程式碼打完,發現沒有程式碼提示了,那些函式裡面的引數和描述都沒有了。對於熟悉Runtime的開發者來說,這並沒有什麼難的,因為引數早已銘記於胸。但是對於新手來說,這是相當不友好的。而且,如果是從iOS6開始開發的同學,依稀可能能感受到,關於Runtime的具體實現的官方文件越來越少了?可能還懷疑是不是錯覺。其實從Xcode5開始,蘋果就不建議我們手動呼叫Runtime的API,也同樣希望我們不要知道具體底層實現。所以IDE上面預設代了一個引數,禁止了Runtime的程式碼提示,原始碼和文件方面也刪除了一些解釋。
具體設定如下:
如果發現匯入了兩個庫檔案之後,仍然沒有程式碼提示,就需要把這裡的設定改成NO,即可。
二. NSObject起源
由上面一章節,我們知道了與Runtime互動有3種方式,前兩種方式都與NSObject有關,那我們就從NSObject基類開始說起。
NSObject的定義如下
Objective-Ctypedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在Objc2.0之前,objc_class原始碼如下:
Objective-Cstruct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在這裡可以看到,在一個類中,有超類的指標,類名,版本的資訊。 ivars是objc_ivar_list成員變數列表的指標;methodLists是指向objc_method_list指標的指標。*methodLists是指向方法列表的指標。這裡如果動態修改*methodLists的值來新增成員方法,這也是Category實現的原理,同樣解釋了Category不能新增屬性的原因。
然後在2006年蘋果釋出Objc 2.0之後,objc_class的定義就變成下面這個樣子了。
Objective-Ctypedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
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
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
把原始碼的定義轉化成類圖,就是上圖的樣子。
從上述原始碼中,我們可以看到,Objective-C 物件都是 C 語言結構體實現的,在objc2.0中,所有的物件都會包含一個isa_t型別的結構體。
objc_object被原始碼typedef成了id型別,這也就是我們平時遇到的id型別。這個結構體中就只包含了一個isa_t型別的結構體。這個結構體在下面會詳細分析。
objc_class繼承於objc_object。所以在objc_class中也會包含isa_t型別的結構體isa。至此,可以得出結論:Objective-C 中類也是一個物件。在objc_class中,除了isa之外,還有3個成員變數,一個是父類的指標,一個是方法快取,最後一個這個類的例項方法連結串列。
object類和NSObject類裡面分別都包含一個objc_class型別的isa。
上圖的左半邊類的關係描述完了,接著先從isa來說起。
當一個物件的例項方法被呼叫的時候,會通過isa找到相應的類,然後在該類的class_data_bits_t中去查詢方法。class_data_bits_t是指向了類物件的資料區域。在該資料區域內查詢相應方法的對應實現。
但是在我們呼叫類方法的時候,類物件的isa裡面是什麼呢?這裡為了和物件查詢方法的機制一致,遂引入了元類(meta-class)的概念。
在引入元類之後,類物件和物件查詢方法的機制就完全統一了。
物件的例項方法呼叫時,通過物件的 isa 在類中獲取方法的實現。 類物件的類方法呼叫時,通過類的 isa 在元類中獲取方法的實現。
meta-class之所以重要,是因為它儲存著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
對應關係的圖如下圖,下圖很好的描述了物件,類,元類之間的關係:
圖中實線是 super_class指標,虛線是isa指標。
- Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
- 每個Class都有一個isa指標指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個迴路。
- 每個Meta class的isa指標都指向Root class (meta)。
我們其實應該明白,類物件和元類物件是唯一的,物件是可以在執行時建立無數個的。而在main方法執行之前,從 dyld到runtime這期間,類物件和元類物件在這期間被建立。具體可看sunnyxx這篇iOS 程式 main 函式之前發生了什麼
(1)isa_t結構體的具體實現
接下來我們就該研究研究isa的具體實現了。objc_object裡面的isa是isa_t型別。通過檢視原始碼,我們可以知道isa_t是一個union聯合體。
Objective-Cstruct objc_object {
private:
isa_t isa;
public:
// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
void initIsa(Class cls /*indexed=false*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
private:
void initIsa(Class newCls, bool indexed, bool hasCxxDtor);
}
那就從initIsa方法開始研究。下面以arm64為例。
Objective-Cinline 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;
}
}
initIsa第二個引數傳入了一個true,所以initIsa就會執行else裡面的語句。
Objective-C# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t indexed : 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;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t indexed : 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;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
ISA_MAGIC_VALUE = 0x000001a000000001ULL轉換成二進位制是11010000000000000000000000000000000000001,結構如下圖:
關於引數的說明:
第一位index,代表是否開啟isa指標優化。index = 1,代表開啟isa指標優化。
在2013年9月,蘋果推出了iPhone5s,與此同時,iPhone5s配備了首個採用64位架構的A7雙核處理器,為了節省記憶體和提高執行效率,蘋果提出了Tagged Pointer的概念。對於64位程式,引入Tagged Pointer後,相關邏輯能減少一半的記憶體佔用,以及3倍的訪問速度提升,100倍的建立、銷燬速度提升。
在WWDC2013的《Session 404 Advanced in Objective-C》視訊中,蘋果介紹了 Tagged Pointer。 Tagged Pointer的存在主要是為了節省記憶體。我們知道,物件的指標大小一般是與機器字長有關,在32位系統中,一個指標的大小是32位(4位元組),而在64位系統中,一個指標的大小將是64位(8位元組)。
假設我們要儲存一個NSNumber物件,其值是一個整數。正常情況下,如果這個整數只是一個NSInteger的普通變數,那麼它所佔用的記憶體是與CPU的位數有關,在32位CPU下佔4個位元組,在64位CPU下是佔8個位元組的。而指標型別的大小通常也是與CPU位數相關,一個指標所佔用的記憶體在32位CPU下為4個位元組,在64位CPU下也是8個位元組。如果沒有Tagged Pointer物件,從32位機器遷移到64位機器中後,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的物件所佔用的記憶體會翻倍。如下圖所示:
蘋果提出了Tagged Pointer物件。由於NSNumber、NSDate一類的變數本身的值需要佔用的記憶體大小常常不需要8個位元組,拿整數來說,4個位元組所能表示的有符號整數就可以達到20多億(注:2^31=2147483648,另外1位作為符號位),對於絕大多數情況都是可以處理的。所以,引入了Tagged Pointer物件之後,64位CPU下NSNumber的記憶體圖變成了以下這樣:
has_assoc
物件含有或者曾經含有關聯引用,沒有關聯引用的可以更快地釋放記憶體
has_cxx_dtor
表示該物件是否有 C++ 或者 Objc 的析構器
shiftcls
類的指標。arm64架構中有33位可以儲存類指標。
原始碼中isa.shiftcls = (uintptr_t)cls >> 3; 將當前地址右移三位的主要原因是用於將 Class 指標中無用的後三位清除減小記憶體的消耗,因為類的指標要按照位元組(8 bits)對齊記憶體,其指標後三位都是沒有意義的 0。具體可以看從 NSObject 的初始化了解 isa這篇文章裡面的shiftcls分析。
magic
判斷物件是否初始化完成,在arm64中0x16是偵錯程式判斷當前物件是真的物件還是沒有初始化的空間。
weakly_referenced
物件被指向或者曾經指向一個 ARC 的弱變數,沒有弱引用的物件可以更快釋放
deallocating
物件是否正在釋放記憶體
has_sidetable_rc
判斷該物件的引用計數是否過大,如果過大則需要其他散列表來進行儲存。
extra_rc
存放該物件的引用計數值減一後的結果。物件的引用計數超過 1,會存在這個這個裡面,如果引用計數為 10,extra_rc的值就為 9。
ISA_MAGIC_MASK 和 ISA_MASK 分別是通過掩碼的方式獲取MAGIC值 和 isa類指標。
Objective-Cinline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
關於x86_64的架構,具體可以看從 NSObject 的初始化了解 isa文章裡面的詳細分析。
(2)cache_t的具體實現
還是繼續看原始碼
Objective-Cstruct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
根據原始碼,我們可以知道cache_t中儲存了一個bucket_t的結構體,和兩個unsigned int的變數。
mask:分配用來快取bucket的總數。
occupied:表明目前實際佔用的快取bucket的個數。
bucket_t的結構體中儲存了一個unsigned long和一個IMP。IMP是一個函式指標,指向了一個方法的具體實現。
cache_t中的bucket_t *_buckets其實就是一個散列表,用來儲存Method的連結串列。
Cache的作用主要是為了優化方法呼叫的效能。當物件receiver呼叫方法message時,首先根據物件receiver的isa指標查詢到它對應的類,然後在類的methodLists中搜索方法,如果沒有找到,就使用super_class指標到父類中的methodLists查詢,一旦找到就呼叫方法。如果沒有找到,有可能訊息轉發,也可能忽略它。但這樣查詢方式效率太低,因為往往一個類大概只有20%的方法經常被呼叫,佔總呼叫次數的80%。所以使用Cache來快取經常呼叫的方法,當呼叫方法時,優先在Cache查詢,如果沒有找到,再到methodLists查詢。
(3)class_data_bits_t的具體實現
原始碼實現如下:
Objective-Cstruct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
}
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class