神經病院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最後就轉變成

// 注意這裏是從父類開始msgSend,而不是從本類開始,謝謝@Josscii 和他同事共同指點出此處描述的不妥。 objc_msgSend(objc_super->receiver, @selector(class)) /// Specifies an instance of a class. 這是類的一個實例 __unsafe_unretained id receiver; // 因爲是實例調用,因此是減號方法 - (Class)class { return object_getClass(self); }

因爲找到了父類NSObject裏面的class方法的IMP,又由於傳入的入參objc_super->receiver = self。self就是son,調用class,因此父類的方法class執行IMP以後,輸出仍是son,最後輸出兩個都同樣,都是輸出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的isa指向是不是本身的類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)

 
// objc_msgSendSuper2() takes the current search class, not its superclass. OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);

objc_msgSendSuper2方法入參是一個objc_super *super。

/// 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 */ }; #endif

因此按viewDidLoad執行時各個變量入棧順序從高到底爲self, _cmd, super_class(等同於self.class), receiver(等同於self), obj。


 

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

在調用self.name的時候,本質上是self指針在內存向高位地址偏移一個指針。


 

從打印結果咱們能夠看到,obj就是cls的地址。在obj向上偏移一個指針就到了0x7fff543f5a90,這正好是ViewController的地址。

因此輸出爲my name is <ViewController: 0x7fb570e2ad00>。

至此,Objc中的對象究竟是什麼呢?

實質:Objc中的對象是一個指向ClassObject地址的變量,即 id obj = &ClassObject , 而對象的實例變量 void *ivar = &obj + offset(N)

加深一下對上面這句話的理解,下面這段代碼會輸出什麼?

- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"ViewController = %@ , 地址 = %p", self, &self); NSString *myName = @"halfrost"; 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: 0x7fff44404ab0> , 地址 = 0x7fff56a48a78 Sark class = Sark 地址 = 0x7fff56a48a50 Void *obj = <Sark: 0x7fff56a48a50> 地址 = 0x7fff56a48a48 my name is halfrost Sark instance = <Sark: 0x6080000233e0> 地址 = 0x7fff56a48a40 my name is (null)

因爲加了一個字符串,結果輸出就徹底變了,[(__bridge id)obj speak];這句話會輸出「my name is halfrost」

緣由仍是和上面的相似。按viewDidLoad執行時各個變量入棧順序從高到底爲self,_cmd,self.class( super_class ),self ( receiver ),myName,obj。obj往上偏移一個指針,就是myName字符串,因此輸出變成了輸出myName了。


 

這裏有一點須要額外說明的是,棧裏面有兩個 self,可能有些人認爲是指針偏移到了第一個 self 了,因而打印出了 ViewController:

my name is <ViewController: 0x7fb570e2ad00>

 

其實這種想法是不對的,從 obj 往上找 name 屬性,徹底是指針偏移了一個 offset 致使的,也就是說指針只往下偏移了一個。那麼怎麼證實指針只偏移了一個,而不是偏移了4個到最下面的 self 呢?


 

obj 的地址是 0x7fff5c7b9a08,self 的地址是 0x7fff5c7b9a28。每一個指針佔8個字節,因此從 obj 到 self 中間確實有4個指針大小的間隔。若是從 obj 偏移一個指針,就到了 0x7fff5c7b9a10。咱們須要把這個內存地址裏面的內容打印出來。

LLDB 調試中,可使用examine命令(簡寫是x)來查看內存地址中的值。x命令的語法以下所示:
x/

n、f、u是可選的參數。
n 是一個正整數,表示顯示內存的長度,也就是說從當前地址向後顯示幾個地址的內容。

f 表示顯示的格式,參見上面。若是地址所指的是字符串,那麼格式能夠是s,若是地十是指令地址,那麼格式能夠是 i。

u 表示從當前地址日後請求的字節數,若是不指定的話,GDB默認是4個bytes。u參數能夠用下面的字符來代替,b表示單字節,h表示雙字節,w表示四字節,g表示八字節。當咱們指定了字節長度後,GDB會從指內存定的內存地址開始,讀寫指定字節,並把其看成一個值取出來。


 

咱們用 x 命令分別打印出 0x7fff5c7b9a10 和 0x7fff5c7b9a28 內存地址裏面的內容,咱們會發現兩個打印出來的值是同樣的,都是 0x7fbf0d606aa0。

這兩個 self 的地址不一樣,裏面存儲的內容是相同的。

因此 obj 是偏移了一個指針,而不是偏移到最下面的 self 。

最後

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

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

做者:一縷殤流化隱半邊冰霜 連接:http://www.jianshu.com/p/9d649ce6d0b8 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索