神經病院Objective-C Runtime入院第一天—isa和Class

一縷殤流化隱半邊冰霜發表於2019-03-04

前言

我第一次開始重視Objective-C Runtime是從2014年11月1日,@唐巧老師在微博上發的一條微博開始。

這是sunnyxx線上下的一次分享會。會上還給了4道題目。

這4道題以我當時的知識,很多就不確定,拿不準。從這次入院考試開始,就成功入院了。後來這兩年對Runtime的理解慢慢增加了,打算今天自己總結總結平時一直躺在我印象筆記裡面的筆記。有些人可能有疑惑,學習Runtime到底有啥用,平時好像並不會用到。希望看完我這次的總結,心中能解開一些疑惑。

目錄

  • 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中獲取資訊,讓物件進行自我檢查。

- (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的類中還定義了一個方法

- (IMP)methodForSelector:(SEL)aSelector;複製程式碼

這個方法會返回指定方法實現的地址IMP。

以上這些方法會在本篇文章中詳細分析具體實現。

3. 通過對 Runtime 庫函式的直接呼叫

關於庫函式可以在Objective-C Runtime Reference中檢視 Runtime 函式的詳細文件。

關於這一點,其實還有一個小插曲。當我們匯入了objc/Runtime.h和objc/message.h兩個標頭檔案之後,我們查詢到了Runtime的函式之後,程式碼打完,發現沒有程式碼提示了,那些函式裡面的引數和描述都沒有了。對於熟悉Runtime的開發者來說,這並沒有什麼難的,因為引數早已銘記於胸。但是對於新手來說,這是相當不友好的。而且,如果是從iOS6開始開發的同學,依稀可能能感受到,關於Runtime的具體實現的官方文件越來越少了?可能還懷疑是不是錯覺。其實從Xcode5開始,蘋果就不建議我們手動呼叫Runtime的API,也同樣希望我們不要知道具體底層實現。所以IDE上面預設代了一個引數,禁止了Runtime的程式碼提示,原始碼和文件方面也刪除了一些解釋。

具體設定如下:

如果發現匯入了兩個庫檔案之後,仍然沒有程式碼提示,就需要把這裡的設定改成NO,即可。

二. NSObject起源

由上面一章節,我們知道了與Runtime互動有3種方式,前兩種方式都與NSObject有關,那我們就從NSObject基類開始說起。

以下原始碼分析均來自objc4-680

NSObject的定義如下


typedef struct objc_class *Class;

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}複製程式碼

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


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不能新增屬性的原因。

關於Category,這裡推薦2篇文章可以仔細研讀一下。
深入理解Objective-C:Category
結合 Category 工作原理分析 OC2.0 中的 runtime

然後在2006年蘋果釋出Objc 2.0之後,objc_class的定義就變成下面這個樣子了。


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)的概念。

關於元類,更多具體可以研究這篇文章What is a meta-class in Objective-C?

在引入元類之後,類物件和物件查詢方法的機制就完全統一了。

物件的例項方法呼叫時,通過物件的 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聯合體。


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為例。


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裡面的語句。



# 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的記憶體圖變成了以下這樣:

關於Tagged Pointer技術詳細的,可以看上面連結那個文章。

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類指標。


inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
    return (Class)(isa.bits & ISA_MASK);
}複製程式碼

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

(2)cache_t的具體實現

還是繼續看原始碼

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的具體實現

原始碼實現如下:



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 nextSiblingClass;

    char *demangledName;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};複製程式碼

在 objc_class結構體中的註釋寫到 class_data_bits_t相當於 class_rw_t指標加上 rr/alloc 的標誌。


class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags複製程式碼

它為我們提供了便捷方法用於返回其中的 class_rw_t *指標:


class_rw_t *data() {
    return bits.data();
}複製程式碼

Objc的類的屬性、方法、以及遵循的協議在obj 2.0的版本之後都放在class_rw_t中。class_ro_t是一個指向常量的指標,儲存來編譯器決定了的屬性、方法和遵守協議。rw-readwrite,ro-readonly

在編譯期類的結構中的 class_data_bits_t *data指向的是一個 class_ro_t *指標:

在執行時呼叫 realizeClass方法,會做以下3件事情:

  1. 從 class_data_bits_t呼叫 data方法,將結果從 class_rw_t強制轉換為 class_ro_t指標
  2. 初始化一個 class_rw_t結構體
  3. 設定結構體 ro的值以及 flag

最後呼叫methodizeClass方法,把類裡面的屬性,協議,方法都載入進來。


struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};複製程式碼

方法method的定義如上。裡面包含3個成員變數。SEL是方法的名字name。types是Type Encoding型別編碼,型別可參考Type Encoding,在此不細說。

IMP是一個函式指標,指向的是函式的具體實現。在runtime中訊息傳遞和轉發的目的就是為了找到IMP,並執行函式。

整個執行時過程可以描述如下:

更加詳細的分析,請看@Draveness 的這篇文章深入解析 ObjC 中方法的結構

到此,總結一下objc_class 1.0和2.0的差別。

三. 入院考試

(一)[self class] 與 [super class]

下面程式碼輸出什麼?

 @implementation Son : Father
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
return self;
}
@end複製程式碼

self和super的區別:

self是類的一個隱藏引數,每個方法的實現的第一個引數即為self。

super並不是隱藏引數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當呼叫方法時,去呼叫父類的方法,而不是本類中的方法。

在呼叫[super class]的時候,runtime會去呼叫objc_msgSendSuper方法,而不是objc_msgSend


OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )


/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};複製程式碼

在objc_msgSendSuper方法中,第一個引數是一個objc_super的結構體,這個結構體裡面有兩個變數,一個是接收訊息的receiver,一個是
當前類的父類super_class。

入院考試第一題錯誤的原因就在這裡,誤認為[super class]是呼叫的[super_class class]。

objc_msgSendSuper的工作原理應該是這樣的:
從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc->receiver去呼叫這個selector。注意,最後的呼叫者是objc->receiver,而不是super_class!

那麼objc_msgSendSuper最後就轉變成


objc_msgSend(objc_super->receiver, @selector(class))複製程式碼

objc_super->receiver = self。所以最後輸出兩個都一樣,都是輸出son。

(二)isKindOfClass 與 isMemberOfClass

下面程式碼輸出什麼?

 @interface Sark : NSObject
 @end

 @implementation Sark
 @end

 int main(int argc, const char * argv[]) {
@autoreleasepool {
    BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
    BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

   NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}複製程式碼

先來分析一下原始碼這兩個函式的物件實現



+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

inline Class 
objc_object::getIsa() 
{
    if (isTaggedPointer()) {
        uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
    return ISA();
}

inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
    return (Class)(isa.bits & ISA_MASK);
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}複製程式碼

首先題目中NSObject 和 Sark分別呼叫了class方法。

+ (BOOL)isKindOfClass:(Class)cls方法內部,會先去獲得object_getClass的類,而object_getClass的原始碼實現是去呼叫當前類的obj->getIsa(),最後在ISA()方法中獲得meta class的指標。

接著在isKindOfClass中有一個迴圈,先判斷class是否等於meta class,不等就繼續迴圈判斷是否等於super class,不等再繼續取super class,如此迴圈下去。

[NSObject class]執行完之後呼叫isKindOfClass,第一次判斷先判斷NSObject 和 NSObject的meta class是否相當,之前講到meta class的時候放了一張很詳細的圖,從圖上我們也可以看出,NSObject的meta class與本身不等。接著第二次迴圈判斷NSObject與meta class的superclass是否相當。還是從那張圖上面我們可以看到:Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身。所以第二次迴圈相等,於是第一行res1輸出應該為YES。

同理,[Sark class]執行完之後呼叫isKindOfClass,第一次for迴圈,Sark的Meta Class與[Sark class]不等,第二次for迴圈,Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等。第三次for迴圈,NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次迴圈,NSObject Class 的super class 指向 nil, 和 Sark Class不相等。第四次迴圈之後,退出迴圈,所以第三行的res3輸出為NO。

如果把這裡的Sark改成它的例項物件,[sark isKindOfClass:[Sark class],那麼此時就應該輸出YES了。因為在isKindOfClass函式中,判斷sark的meta class是自己的元類Sark,第一次for迴圈就能輸出YES了。

isMemberOfClass的原始碼實現是拿到自己的isa指標和自己比較,是否相等。
第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都輸出NO。

(三)Class與記憶體地址

下面的程式碼會?Compile Error / Runtime Crash / NSLog…?

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name`s %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls
    [(__bridge id)obj speak];
}
@end複製程式碼

這道題有兩個難點。難點一,obj呼叫speak方法,到底會不會崩潰。難點二,如果speak方法不崩潰,應該輸出什麼?

首先需要談談隱藏引數self和_cmd的問題。
當[receiver message]呼叫方法時,系統會在執行時偷偷地動態傳入兩個隱藏引數self和_cmd,之所以稱它們為隱藏引數,是因為在原始碼中沒有宣告和定義這兩個引數。self在上面已經講解明白了,接下來就來說說_cmd。_cmd表示當前呼叫方法,其實它就是一個方法選擇器SEL。

難點一,能不能呼叫speak方法?


id cls = [Sark class]; 
void *obj = &cls;複製程式碼

答案是可以的。obj被轉換成了一個指向Sark Class的指標,然後使用id轉換成了objc_object型別。obj現在已經是一個Sark型別的例項物件了。當然接下來可以呼叫speak的方法。

難點二,如果能呼叫speak,會輸出什麼呢?

很多人可能會認為會輸出sark相關的資訊。這樣答案就錯誤了。

正確的答案會輸出


my name is <ViewController: 0x7ff6d9f31c50>複製程式碼

記憶體地址每次執行都不同,但是前面一定是ViewController。why?

我們把程式碼改變一下,列印更多的資訊出來。


- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"ViewController = %@ , 地址 = %p", self, &self);

    id cls = [Sark class];
    NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);

    void *obj = &cls;
    NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);

    [(__bridge id)obj speak];

    Sark *sark = [[Sark alloc]init];
    NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);

    [sark speak];

}複製程式碼

我們把物件的指標地址都列印出來。輸出結果:


ViewController = <ViewController: 0x7fb570e2ad00> , 地址 = 0x7fff543f5aa8
Sark class = Sark 地址 = 0x7fff543f5a88
Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80

my name is <ViewController: 0x7fb570e2ad00>

Sark instance = <Sark: 0x7fb570d20b10> 地址 = 0x7fff543f5a78
my name is (null)複製程式碼

按viewDidLoad執行時各個變數入棧順序從高到底為self, _cmd, self.class, self, obj。

第一個self和第二個_cmd是隱藏引數。第三個self.class和第四個self是[super viewDidLoad]方法執行時候的引數。

在呼叫self.name的時候,本質上是self指標在記憶體向高位地址偏移一個指標。在32位下面,一個指標是4位元組=4*8bit=32bit。

從列印結果我們可以看到,obj就是cls的地址。在obj向上偏移32bit就到了0x7fff543f5aa8,這正好是ViewController的地址。

所以輸出為my name is

入院考試由於還有一題沒有解答出來,所以醫院決定讓我住院一天觀察。

未完待續,請大家多多指教。

相關文章