1. 程式人生 > >神經病院 Objective-C Runtime 入院第一天—— isa 和 Class

神經病院 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-C
typedef struct objc_class *Class;

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

在Objc2.0之前,objc_class原始碼如下:

Objective-C
struct 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-C
typedef 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指標。

  1. Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
  2. 每個Class都有一個isa指標指向唯一的Meta class
  3. Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個迴路。
  4. 每個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-C
struct 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-C
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;
    }
}

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-C
inline Class  
objc_object::ISA()  
{
    assert(!isTaggedPointer()); 
    return (Class)(isa.bits & ISA_MASK);
}

關於x86_64的架構,具體可以看從 NSObject 的初始化了解 isa文章裡面的詳細分析。

(2)cache_t的具體實現

還是繼續看原始碼

Objective-C
struct 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-C
struct 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