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

前言

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

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

這4道題以我當時的知識,不少就不肯定,拿不許。從此次入院考試開始,就成功入院了。後來這兩年對Runtime的理解慢慢增長了,打算今天本身總結總結平時一直躺在我印象筆記裏面的筆記。有些人可能有疑惑,學習Runtime到底有啥用,平時好像並不會用到。但願看完我此次的總結,心中能解開一些疑惑。github

目錄

  • 1.Runtime簡介
  • 2.NSObject起源
    • (1) isa_t結構體的具體實現
    • (2) cache_t的具體實現
    • (3) class_data_bits_t的具體實現
  • 3.入院考試

一. Runtime簡介

Runtime 又叫運行時,是一套底層的 C 語言 API,是 iOS 系統的核心之一。開發者在編碼過程當中,能夠給任意一個對象發送消息,在編譯階段只是肯定了要向接收者發送這條消息,而接受者將要如何響應和處理這條消息,那就要看運行時來決定了。objective-c

C語言中,在編譯期,函數的調用就會決定調用哪一個函數。 而OC的函數,屬於動態調用過程,在編譯期並不能決定真正調用哪一個函數,只有在真正運行時纔會根據函數的名稱找到對應的函數來調用。swift

Objective-C 是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。vim

Objc 在三種層面上與 Runtime 系統進行交互:緩存

1. 經過 Objective-C 源代碼

通常狀況開發者只須要編寫 OC 代碼便可,Runtime 系統自動在幕後把咱們寫的源代碼在編譯階段轉換成運行時代碼,在運行時肯定對應的數據結構和調用具體哪一個方法。數據結構

2. 經過 Foundation 框架的 NSObject 類定義的方法

在OC的世界中,除了NSProxy類之外,全部的類都是NSObject的子類。在Foundation框架下,NSObject和NSProxy兩個基類,定義了類層次結構中該類下方全部類的公共接口和行爲。NSProxy是專門用於實現代理對象的類,這個類暫時本篇文章不提。這兩個類都遵循了NSObject協議。在NSObject協議中,聲明瞭全部OC對象的公共方法。架構

在NSObject協議中,有如下5個方法,是能夠從Runtime中獲取信息,讓對象進行自我檢查。app

- (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

入院考試因爲還有一題沒有解答出來,因此醫院決定讓我住院一天觀察。

未完待續,請你們多多指教。

相關文章
相關標籤/搜索