本文的切入點是2014年的一場線下分享會,也就是sunnyxx分享的objc runtime。很慚愧,這麼多年了才完整的看了一下這個分享會視頻。當時他出了一份試題,並戲稱精神病院objc runtime入院考試。git
咱們今天的這篇文章就是從這個試題中的題目入手,來深刻的學習runtime。github
源碼版本objc4-750工具
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
第一行的[self class]
應該是沒有疑問的,確定是Son
,問題就出在這個[super class]
。學習
你們都知道,咱們OC的方法在底層會編譯爲一個objc_msgSend
的方法(消息發送),[self class]
符合這個狀況,由於self是類的一個隱藏參數。可是super
並非一個參數,它是一個關鍵字,其實是一個「編譯器標示符」,因此這就有點不同了,經查閱資料,在調用[super class]
的時候,runtime調用的是objc_msgSendSuper
方法,而不是objc_msgSend
。測試
首先要作的是驗證一下是不是調用了objc_msgSendSuper
。這裏用到了clang這個工具,咱們能夠把OC的代碼轉成C/C++。atom
@implementation Son - (void)test { [super class]; } @end
在終端運行clang -rewrite-objc Son.m
生成一個Son.cpp文件。spa
在這個.cpp文件的底部咱們能夠找到這麼一部分代碼指針
// @implementation Son static void _I_Son_test(Son * self, SEL _cmd) { ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class")); } // @end
看起來亂七八糟,有不少強制類型轉換的代碼,不用理它,咱們只要看到了咱們想要的objc_msgSendSuper
就好。code
去源碼中看一下這個方法(具體實現好像是彙編,看不懂)orm
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
能夠看出來這個方法第一個參數是一個objc_super
類型的結構體,第二個是一個咱們常見的SEL,後面的...表明還有擴展參數。
再看一下這個objc_super
結構體。
/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull 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 _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif /* super_class is the first class to search */ };
第一個參數是接收消息的receiver,第二個是super_class(見名知意~ 😆)。咱們和上面提到的.cpp中的代碼對應一下就會發現重點了,receiver是self。
因此,這個[super class]
的工做原理是,從objc_super
結構體的super_class
指向類的方法列表開始查找class
方法,找到這個方法以後使用receiver
來調用。
因此,調用class
方法的其實仍是self
,結果也就是打印Son
。
下面代碼的結果?
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]];
對於這個問題咱們就要從OC類的結構開始提及了。
咱們都應該有所瞭解,每個Objective-c的對象底層都是一個C語言的結構體,在以前老的源碼中體現出,全部對象都包含一個isa
類型的指針,在新的源碼中已經不是這樣了,用一個結構體isa_t
代替了isa
。這個isa_t
結構體包含了當前對象指向的類的信息。
咱們來看看當前的類的結構,首先從咱們的祖宗類NSObject開始吧。
@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; }
咱們的NSObject類有一個Class類型的變量isa,經過源碼咱們能夠了解到這個Class究竟是什麼
typedef struct objc_class *Class; typedef struct objc_object *id; 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 }
上面的代碼是我從源碼中複製拼到一塊兒來的。能夠看出來,Class就是是一個objc_class結構體,objc_class中有四個成員變量Class superclass
,cache_t cache
,class_data_bits_t bits
,和從objc_object
中繼承過來的isa_t isa
。
當Objc爲一個對象分配內存,初始化實例變量後,在這些實例變量的結構體中第一個就是isa。
並且從上面的objc_class的結構能夠看出來,不只僅是實例會包含一個isa結構體,全部的類也會有這個isa。
因此說,咱們能夠得出這樣一個結論:Objective-c中的類也是一個對象。
那如今就有了一個新的問題,類的isa結構體中儲存的是什麼?這裏就要引入一個元類
的概念。
知識補充:
在Objective-c中,每一個對象能執行的方法並無存在這個對象中,由於若是每個對象都單獨儲存可執行的方法,那對內存來講是一個很大的浪費,因此說每一個對象可執行的方法,也就是咱們說的一個類的實例方法,都儲存在這個類的objc_class
結構體中的class_data_bits_t
結構體裏面。在執行方法是,對象經過本身的isa找到對應的類,而後在class_data_bits_t
中查找方法實現。
關於方法的結構,能夠看這篇博客來理解一些。(跳轉連接)
引入元類就是來保證了實例方法和類方法查找調用機制的一致性。
因此讓一個類的isa指向他的元類,這樣的話,對象調用實例方法能夠經過isa找到對應的類,而後查找方法的實現並調用,在調用類方法的時候,經過類的isa找到對應的元類,在元類裏完成類方法的查找和調用。
下面這種圖也是在網上很常見的了,不須要過多解釋,你們看一下記住就好了。
看到這裏咱們就要回到咱們的題目上了。首先呢,仍是要去看一下這個源碼中isKindOfClass:
和isMemberOfClass:
的實現了。
先看isKindOfClass
吧,源碼中提供了一個類方法一個實例方法。
+ (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; }
整體的邏輯都是同樣的,都是先聲明一個Class類型的tcls,而後把這個tcls跟cls比較,看是否相等,若是不相等則循環tcls的各級superclass來進行比較,直到爲tcls爲nil中止循環。
不一樣的地方就是類方法初始的tcls是object_getClass((id)self)
,實例方法的是[self class]
。
object_getClass((id)self)
實際上是返回了這個self的isa對應的結構,由於這個方法是在類方法中調用的,self則表明這個類,那object_getClass((id)self)
返回的也應該是這個類的元類了。
其實在-isKindOfClass
這個實例方法中,調用方法的是一個對象,tcls初始等於[self class]
,也就是對相對應的類。咱們能夠看出來,在實例方法中這個tcls初始的值也是方法調用者的isa對應的結構,跟類方法中邏輯是一致的。
回到咱們的題目中,
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
[NSObject class]
也就是NSObject類調用這個isKindOfClass:
方法(類方法),方法的參數也是NSObject的類。
在第一次循環中,tcls對應的應該是NSObject的isa指向的,也就是NSObject的元類,它跟NSObject類不相等。第二次循環,tcls取本身的superclass繼續比較,咱們上面的那個圖,你們能夠看一下,NSObject的元類的父類就是NSObject這個類自己,在與NSObject比較結果是相等。因此res1爲YES。
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
跟上面同樣來分析,在第一次循環中,tcls對應的應該是Sark的isa指向的,也就是Sark的元類,跟Sark的類相比,確定是不相等。第二次循環,tcls取superclass,從圖中能夠看出,Sark元類的父類是NSObject的元類,跟Sark的類相比,確定也是不相等。第三次循環,NSObject元類的父類是NSObject類,也不相等。再取superclass,NSObject的superclass爲nil,循環結束,返回NO,因此res3是NO。
+ (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; }
有了上面isKindOfClass邏輯分析的基礎,isMemberOfClass的邏輯咱們應該很清楚,就是使用方法調用者的isa對應的結構和傳入的cls參數比較。
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSObject類的isa對應的是NSObject的元類,和NSObject類相比不相等,因此res2爲NO。
Sark類的isa對應的是Sark的元類,和Sark類相比也是不相等,因此,res4也是NO。
下面的代碼會?Compile Error / Runtime Crash / NSLog…?
@interface NSObject (Sark) + (void)foo; @end @implementation NSObject (Sark) - (void)foo { NSLog(@"IMP: -[NSObject (Sark) foo]"); } @end // 測試代碼 [NSObject foo]; [[NSObject new] foo];
[[NSObject new] foo];
這一個代碼應該是毫無疑問會調用到-foo
方法。問題就在這個[NSObject foo]
,由於在咱們的認識中[NSObject foo]
是調用的類方法,實現的是實例方法,應該不能調用到。
其實這個題的考點跟第二個題差很少,咱們已經知道了,一個類的實例方法儲存在類中,類方法儲存在這個類的元類。因此NSObject在調用foo這個方法是,會先去NSObject的元類中找這個方法,沒有找到,那就要去父類中繼續查找。上面圖已經給出了,NSObject的元類的父類是NSObject類,因此在NSObject中查找方法,找到方法以後執行打印。
下面的代碼會?Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject @property (nonatomic, copy) NSString *name; @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
這裏咱們先上結果:
my name's <ViewController: 0x7f9454c1c680>
無論地址是多少,打印的老是ViewController。
咱們先想一下爲何能夠成功的調用speak?id cls = [Sark class];
建立了一個Sark的class。void *obj = &cls;
建立一個obj指針指向了cls的地址。最後使用(__bridge id)obj
把這個obj指針轉成一個oc的對象,用對象來調用speak,因此能夠調用成功。
咱們在方法中輸出的是self.name
,爲何會打印出來ViewController?
通過查閱資料得知,在調用self.name的時候,本質上是self指針在內存向高位地址偏移一個指針。(這個還得之後深刻研究)
爲了驗證一下查到的這個結論,我改寫了一下speak
方法中的代碼以下。
- (void)speak { unsigned int count = 0; Ivar * ivars = class_copyIvarList([self class], &count); for (int i = 0; i < count; i ++) { Ivar ivar = ivars[i]; ptrdiff_t offSet = ivar_getOffset(ivar); const char * n = ivar_getName(ivar); NSLog(@"%@-----%ld",[NSString stringWithUTF8String:n],offSet); } NSLog(@"my name's %@", self.name); }
取到類的各個變量,而後打印出他的偏移。輸出結構以下:
_name-----8
偏移了一個指針。
那爲何打印出來了ViewController的地址,咱們就要研究各個變量的內存地址位置關係了。
在iewDidLoad
中變量的壓棧順序以下所示:
第一個參數self和第二個參數_cmd是隱藏參數,第三和第四個參數是執行[super viewDidLoad]
以後進棧的,以前第一題的時候咱們有了解過,super調用的方法在底層編譯以後會有一個objc_super
類型的結構體。在結構體中有receiver和super_class兩個變量,receiver就是self。
我在網上查過不少的資料,都是super_class比receiver(self)先入棧,不太懂爲何是super_class先入。
最後是生成的obj進棧。
因此在打印self.name的時候,是obj的指針向高位偏移了一個指針,也就是self,因此打印出來的是ViewController的指針。
https://github.com/draveness/...
http://blog.sunnyxx.com/2014/...