1. 程式人生 > Android開發 >面試遇到Runtime的第二天-isa和meta-Class

面試遇到Runtime的第二天-isa和meta-Class

本文主要寫一下,runtime中關於類,元類的結構和他們之間的關係。其實應該在上一篇文章面試遇到Runtime的第一天中先寫本文的內容,但是寫那天剛好在整理category的知識點,所以趁熱打鐵的就寫在了上一篇文章。如果在閱讀時遇到有比較難理解的點,不妨可以先閱讀本文,再去閱讀面試遇到Runtime的第一天中的內容。

runtime簡介

閱讀過面試遇到Runtime的第一天,你肯定就已經知道,runtime通俗點說就是一套底層API,那麼我們通過什麼方式可以呼叫到它呢?

  1. OC(Objective-C 後文簡稱OC)上層方法直接呼叫

我們編寫的OC程式碼,在編譯階段會自動轉換成執行時程式碼

  1. NSObject API

NSObject可以看做是所有類的基類(NSProxy除外),NSObject協議中定義瞭如下這些可以從runtime中獲取資訊的方法

- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
複製程式碼
  1. runtime api

當然我們也可以手動呼叫runtime API,需要匯入objc/Runtime.h和objc/message.h兩個標頭檔案

NSObject

從NSObject原始碼入手

先看NSObject的定義

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

複製程式碼

只有一個Class型別的isa,找到Class的定義,是一個objc_class的結構體

typedef struct objc_class *Class;
複製程式碼

這裡我們直接看Objc2.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;
}

複製程式碼

從原始碼中我們可以證實以下幾點:

  • objc_object被typedef為id,也就是我們常見的id型別,id 這個struct的定義本身就帶了一個 * 所以我們在使用其他NSObject型別的例項時需要在前面加上\ *, 而使用 id 時卻不用。
  • objc_class繼承自objc_object,所以OC中的類也是一個物件,而類物件的isa則指向meta class
  • object類和NSObject類裡面分別都包含一個objc_class型別的isa

物件,類,元類

這張圖中的關係我們需要好好的理解(並且記憶)一下,也有助於我們之後對runtime的理解

  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)。
元類

元類這個概念比較抽象,他為什麼存在呢?可以從呼叫類方法開始說起:

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];
複製程式碼

上面這句程式碼可以正確執行,說明OC不僅可以給物件傳送訊息,也同樣可以給類傳送訊息,因為上面我們也提到了,OC中類也是一個物件
那麼,當給類傳送訊息的時候,類物件的isa就指向了meta-class,所以要去meta-class中去找到該方法的實現。
可以理解為,meta-class的存在是為了統一OC中所有物件訊息查詢轉發的流程,或者說是引入meta-class來保證無論是類還是物件都能通過相同的機制查詢方法的實現。

  • 例項方法呼叫時,通過物件的 isa 在類中獲取方法的實現
  • 類方法呼叫時,通過類的 isa 在元類中獲取方法的實現

這裡又引出了幾個面試題:
問:物件的例項方法存在哪兒?類方法存在哪兒?
答:當一個物件的例項方法被呼叫時,會通過isa找到對應的類,然後在該類的class_data_bits_t中去查詢方法對應的實現,所以例項方法是存在類中的,而類方法是存在元類中的。

問:類方法在元類中是以什麼形式存在?
答:類方法在元類中是以例項方法存在,並且,物件在類中是一個例項,類在元類中也是一個例項。所以,類的類方法,就是元類的例項方法。(這裡比較繞,多讀幾遍,好好理解)

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的作用主要是為了加速訊息分發, 系統會對方法和對應的地址進行快取,所以在實際執行中,大部分常用的方法都是會被快取起來的,Runtime系統實際上非常快,接近直接執行記憶體地址的程式速度。

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;
    }
};

複製程式碼

這兩部分本文不做過多的闡述,後面在寫訊息查詢轉發的時候在詳細介紹,這裡先簡單瞭解一下即可。

實戰

上面囉裡囉嗦的寫了一堆,看文章看到這裡可能也是似懂非懂的樣子,下面通過我們最常用的幾個方法,來實戰一下上面講解的理論知識,幫助我們理解記憶。

[self class] 與 [super class]

如下程式碼列印結果是什麼?

@interface Sark : NSObject
複製程式碼
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@",NSStringFromClass([self class]));
        NSLog(@"%@",NSStringFromClass([super class]));
    }
    return self;
}
複製程式碼

列印結果:

14:17:56.444058+0800 test[30316:787077] Sark
14:17:56.444177+0800 test[30316:787077] Sark
複製程式碼

解釋一下為什麼NSLog(@"%@",NSStringFromClass([super class]));也輸出了Sark

  1. self 不一定是當前類,self只是一個形參 objc_msgSend(id self,SEL _cmd) 取決於訊息的接收者
  2. 在呼叫[super class]的時候,runtime會去呼叫objc_msgSendSuper方法

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。

  1. objc_msgSendSuper的工作原理應該是這樣的: 從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc->receiver去呼叫父類的這個selector。注意,最後的呼叫者是objc->receiver = self,而不是super_class!

isKindOfClass 與 isMemberOfClass

如下程式碼列印結果是什麼?

    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);// yes no no no 

複製程式碼

檢視isKindOfClass和isMemberOfClass的原始碼:

+ (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) {// 第一次就獲取到了class -> meta class  然後再遍歷superclass

        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {//第一次獲取了self -> class  然後再遍歷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;
}

複製程式碼

分析:

  1. isKindOfClass 區分例項方法和類方法的實現不同 類方法迴圈首先取了isa指標 第一次就獲取到了class -> meta class 然後再遍歷superclass 而例項方法是第一次獲取了self -> class 然後再遍歷superclass
  2. isMemberOfClass 沒有遍歷 直接比較

因此,第一行程式碼 BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];的判斷流程是:

  • [NSObject class] --> NSObject
  • NSObject 和 NSObject meta-Class 不相等 然後對NSObject meta-Class取super class
  • NSObject 和 NSObject 相等 返回 yes

對NSObject meta-Class取super class用到裡圖裡紅圈標識出來的關係,得到結果是NSObject

第二行程式碼 BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];的分析流程是:

  • [NSObject class] --> NSObject
  • object_getClass(NSObject) --> NSObject meta-Class
  • NSObject 和 NSObject meta-Class不相等 返回 NO

第三行程式碼 BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]]; Sark取meta-Class然後再一直遍歷找super_class,最終也不會找到相等的,返回NO

第四行程式碼 BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]]; Sark和Sark meta-Class不相等,返回NO

總結

經過上面兩個小問題的實戰,是不是對物件、類、元類之間的關係有了更深刻的理解,簡單總結一下:

  1. 每個類都有一個唯一對應的元類
  2. 類物件的isa指向了元類
  3. 元類中以例項方法的形式儲存了類的類方法
  4. 根元類(Root meta class)的isa指向自己,super class為NSObject,形成一個閉環