深入學習runtime
本文的切入點是2014年的一場線下分享會,也就是sunnyxx分享的objc runtime。很慚愧,這麼多年了才完整的看了一下這個分享會視訊。當時他出了一份試題,並戲稱精神病院objc runtime入院考試。
我們今天的這篇文章就是從這個試題中的題目入手,來深入的學習runtime。
原始碼版本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++。
@implementation Son - (void)test { [super class]; } @end
在終端執行 clang -rewrite-objc Son.m
生成一個Son.cpp檔案。
在這個.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
就好。
去原始碼中看一下這個方法(具體實現好像是彙編,看不懂)
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(見名知意~ :laughing:)。我們和上面提到的.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 isaOBJC_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
先看 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。
isMemberOfClass
+ (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/...