深刻學習runtime

本文的切入點是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 superclasscache_t cacheclass_data_bits_t bits,和從objc_object中繼承過來的isa_t isa

當Objc爲一個對象分配內存,初始化實例變量後,在這些實例變量的結構體中第一個就是isa。

objc-isa-class-object.png

並且從上面的objc_class的結構能夠看出來,不只僅是實例會包含一個isa結構體,全部的類也會有這個isa。

因此說,咱們能夠得出這樣一個結論:Objective-c中的類也是一個對象。

那如今就有了一個新的問題,類的isa結構體中儲存的是什麼?這裏就要引入一個元類的概念。

知識補充:
在Objective-c中,每一個對象能執行的方法並無存在這個對象中,由於若是每個對象都單獨儲存可執行的方法,那對內存來講是一個很大的浪費,因此說每一個對象可執行的方法,也就是咱們說的一個類的實例方法,都儲存在這個類的objc_class結構體中的class_data_bits_t結構體裏面。在執行方法是,對象經過本身的isa找到對應的類,而後在class_data_bits_t中查找方法實現。

關於方法的結構,能夠看這篇博客來理解一些。(跳轉連接

引入元類就是來保證了實例方法和類方法查找調用機制的一致性。

因此讓一個類的isa指向他的元類,這樣的話,對象調用實例方法能夠經過isa找到對應的類,而後查找方法的實現並調用,在調用類方法的時候,經過類的isa找到對應的元類,在元類裏完成類方法的查找和調用。

下面這種圖也是在網上很常見的了,不須要過多解釋,你們看一下記住就好了。

objc-isa-class-diagram.png

看到這裏咱們就要回到咱們的題目上了。首先呢,仍是要去看一下這個源碼中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中變量的壓棧順序以下所示:

壓棧順序.png

第一個參數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/...

https://www.jianshu.com/p/743...

https://github.com/ming1016/s...

相關文章
相關標籤/搜索