runtime中元類的理解

參考文章

清晰理解Objective-C元類
object_getClass(obj)與[obj class]的區別-源代碼解析
刨根問底Objective-C Runtime

元類

這幾天看了一些runtime底層的一些介紹,感受最深的就是對元類的講解了。突然有種恍然大悟的感覺,以前我們經常說一切皆對象,在這里體現出來了。

1. 元類是什么

眾所周知Objective-C(以下簡稱OC)中的消息機制,消息的接收者可以是一個對象,也可以是一個類。那么這兩種情況統一為一種情況不是更方便嗎?蘋果當然早就想到了,這也正是元類的用處。蘋果統一把消息接收者作為對象。等等,這也是說類也是對象?yes,就是這樣。就是說,OC中所有的類都是一種對象。由一個類實例化來的對象叫實例對象,這好理解,那么類作為對象(稱之為類對象),又是什么類的對象呢?當然也容易猜到,就是元類(MetaClass)。現在到給元類下定義的時候了:元類就是類對象所屬的類。所以,實例是類的實例,類作為對象又是元類的實例。已經說了,OC中所有的類都是一種對象,所以元類也是對象,那么元類是什么的實例呢?答曰:根元類,同時根元類是其自身的實例。

上面講到了實例對象、類對象、元類對象,有什么區別?
實例對象:當我們在代碼中new一個實例對象時,拷貝了實例所屬的類的成員變量,但不拷貝類定義的方法,調用實例方法時,調用實例的isa指針去尋找方法對應的函數指針。
類對象:是一個功能完整的對象,特殊之處在于它們是由程序員定義而在運行時由編譯器創建的,它沒有自己的實例變量(這里區別于類的成員變量,它們是屬于實例對象的,而不是屬于類對象的,類方法是屬于類對象自己的),但類對象中存著成員變量和實例方法列表。
元類對象:OC的類方法是使用元類的根本原因,因為其中存儲著對應的類對象調用的方法即類方法。其他時候都傾向于隱藏元類,因此真實世界沒有人發送消息給元類對象。元類的定義和創建看起來都是編譯器自動完成的,無須人為干涉。要獲取一個類的元類,可使用如下定義的函數:

Class objc_getMetaClass(const char *name); // name為類的名字

此外還有一個獲取對象所屬的類的函數:

Class object_getClass(id obj);

由于類對象是元類的實例,所以當傳入的參數為類名時,返回的就是指向該類所屬元類的指針。

2. 元類的構建機制

既然所有的類都是對象,那么元類又是什么類的對象?這樣下去,不是子子孫孫無窮盡也?當然不行,OC作為一門編程語言,當然要滿足完備性----既定的各條規則都要滿足,不能相互矛盾,也不能存在漏洞。
OC作為運行時語言,上面提到的類與元類在運行時都是objc_class類型。在Objective-C2.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;
/* Use `Class` instead of `struct objc_class *` */

其中,Class定義如下:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

可以看出,OC中每個類都包含一個isa變量,顯然這里的isa是指向另一個類的指針,說白了就是表面這個類是哪個類的實例,以便找到代碼中調用的本類或父類的類方法。對于NSObject及其子類,指向的就是它的元類,正如實例中也有個isa指針指向其所屬的類一樣。而對于元類,每個元類的isa指針都指向根元類。那么根元類的isa指向哪里?---它自己。這樣就構成了一個封閉的循環,實現了無懈可擊的OC類系統。這種關系在下面的圖中有清晰的體現。

除了isa聲明了實例與所屬類的關系,還有super_class聲明了類、元類的繼承關系。每個類對象都有對應的元類,每個類(根類除外)都有一個superclass,同樣每個元類也有一個superclass,并且子類與子元類、父類與父元類分別在同一個層次。這種關系借用網上的一張圖來說明,一目了然。



注意:根元類的superclass不是nil而是根類。對于OC原生的類,根元類的父類就是系統的根類NSObject。但根類不一定是NSObject,因為后面介紹的objc_allocateClassPair函數也可以創建出一個根類。

3. 元類的構建機制

上面講到了OC運行時類的定義,這里可以看看對象的定義,重點關注objc_object 和 id。

#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

這說明OC中每個對象也都包含一個isa變量,這里的isa,指向實例對象所屬的類。在運行時,[obj aMessage];被轉化為objc_msgSend(obj, @selector(aMessage));,這里,@selector(aMessage)返回一個SEL數據類型,即方法選擇器。SEL主要作用是快速的通過方法名字(aMessage)查找到對應方法的函數指針,然后調用其函數。SEL其本身是一個int型的地址,地址中存放著方法的名字。在一個類中,每一個方法對應著一個SEL。iOS類中不能存在兩個名稱相同的方法,即使參數類型不同,因為SEL是根據方法名生成,相同的方法名稱只能對應一個SEL。

當一個消息發送給任何一個對象,方法的檢查器從對象的isa指針開始,然后是父類。具體地,在objc_msgSend函數中,首先通過obj的isa指針找到obj對應的class。在class中,有一塊最近調用的方法的指針緩存,所以先去cache通過selector查找對應的method,若cache中未找到,再去method list中查找,若method list中未找到,則去superClass中查找。若能找到,則將method加入到cache中,以方便下次查找,并通過method中的函數指針跳轉到對應的函數中去執行。對最后一句不了解的話,請看objc_method定義:

struct objc_method { 
??SEL method_name; // 方法名稱 
??const char *method_typesE; // 參數和返回類型的描述字串 
??IMP method_imp; // 方法的具體的實現的指針 
}

由上看出,OC中實例方法是通過isa找到object所屬的class,再在class中找到要調用的method。此可得出結論:class即類對象中存儲著實例方法。實際上,類對象中存儲著類定義的一切:成員變量、屬性列表、遵守的協議等,但不包括類方法。類方法的定義在哪?就是在元類里面。此外元類中還存在著類的信息(類的版本,名字),比如發送一個類消息[class aMessage];,class中的isa就指向class的元類,在元類中搜索調用的類方法,搜索層次類似于實例方法的搜索。

4. 元類的應用

類對象和元類對象的相關方法:

  1. object_getClass跟隨實例的isa指針,返回此實例所屬的類,對于實例對象(instance)返回的是類(class),對于類(class)則返回的是元類(metaclass);
  2. -class方法對于實例對象(instance)會返回類(class),但對于類(class)則不會返回元類(metaclass),而只會返回類本身,即[@"instance" class]返回的是__NSCFConstantString,而[NSString class]返回的是NSString。
  3. class_isMetaClass可判斷某類是否為元類。
  4. 使用objc_allocateClassPair可在運行時創建新的類與元類對,使用class_addMethod和class_addIvar可向類中增加方法和實例變量,最后使用objc_registerClassPair注冊后,就可以使用此類了。這體現了OC作為運行時語言的強大之一:在代碼中動態創建類并添加方法。
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(addedMethod), (IMP)added_method_implementation, "v@:");
void added_method_implementation(id self, SEL __cmd)
{
    // do something
}

說明:objc_allocateClassPair函數的作用是創建一個新類newClass及其元類,三個參數依次為newClass的父類,newClass的名稱,第三個參數通常為0。然后可向newClass中添加變量及方法,注意若要添加類方法,需用objc_getClass(newClass)獲取元類,然后向元類中添加類方法。接下來必須把newClass注冊到運行時系統,否則系統不能識別這個類。

  1. 根據以上理解比較一下object_getClass(obj)和[obj class]的區別
    其實很簡單,直接看源代碼吧。
    object_getClass(obj)的代碼實現:
Class object_getClass(id obj)
{
    return _object_getClass(obj);
}

其中_object_getClass(obj)是一個靜態內聯函數,代碼實現如下:

static inline Class _object_getClass(id obj)
{
    #if SUPPORT_TAGGED_POINTERS
    if (OBJ_IS_TAGGED_PTR(obj)){
        uint8_t slotNumber = ((uint8_t)(uint64_t) obj) & 0x0F;
        Class isa = _objc_tagged_isa_table[slotNumber];
        return isa;
    }
    #endif
        if (obj) return obj->isa;
        else return Nil;
}

簡單的說_object_getClass函數就是返回對象的isa指針。
[obj class]的代碼實現分為兩種情況,分別是obj為實例對象和類對象,代碼如下所示:

// 類方法直接返回自身指針
+ (Class)class
{
   return self;
 }
// 實例方法調用object_getClass,返回isa指針
- (Class)class 
{
    return object_getClass(self);
}

通過以上代碼可以看出,調用[obj class],不管obj對實例對象還是類對象,結果都是一樣的。

  1. 看下這幾個個面試題吧



    第一題解析如下:
    在調用[self class]時,會轉化為objc_msgSend函數。函數定義如下:

id objc_msgSend(id self, SEL op, ...)

我們把self做為第一個參數傳遞進去。而在調用[super class]時,會轉化為objc_msgSendSuper函數??聪潞瘮刀x:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一個參數是objc_super這樣一個結構體,其定義如下:

struct objc_super {
    __unsafe_unretained id receiver;
    __unsafe_unretained Class super_class;  
};

結構體有兩個成員,第一個成員是receiver,類似于上面的objc_msgSend函數第一個參數self。第二個成員是記錄當前類的父類是什么。
所以當調用[self class]時,實際先調用的是objc_msgSend函數,第一個參數是Son當前的這個實例,然后在Son這個類里面去找-(Class)class這個方法,沒有就去父類Father里找,也沒有,最后在NSObject類中發現這個方法。而-(Class)class的實現就是返回self的類別,故上述輸出結果為Son。
objc Runtime開源代碼對- (Class)class方法的實現:

- (Class)class {
    return object_getClass(self);
}

而當調用[super class]時,會轉換為objc_msgSendSuper函數。第一步先構造objc_super結構體,結構體第一個成員就是self,第二個成員是(id)class_getSuperclass(objc_getClass("son")),實際該函數輸出結果為Father。第二部是去Father這個類里去找- (Class)class,沒有,然后去NSObject類去找。找到了,最后內部是使用objc_msgSend(objc_super->receiver, @selector(class))去調用,此時已經和[self class]調用相同,故上述輸出結果仍然返回Son。
第二題解析如下:
運行結果是
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0
對于
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject calss]];
首先還是看看isKindOfClass的實現:

- (BOOL)isKindOfClass: aClass
{
    Class cls;
    for (cls = isa; cls; cls = cls->superclass) 
        if (cls == (Class)aClass)
            return YES;
    return NO;
}

當[NSObject class]對象第一次進行比較的時候,得到它的isa為NSObject的Meta Class,這個時候NSObject Meta Class 和 NSObject Class不相等。然后取出NSObject的Meta Class的Super class,這個時候又變成了NSObject Class,所以相等。
對于
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject calss]];
先看看isMemberOfClass:的實現吧,

- (BOOL)isMemberOf:aClass
{
    return isa == (Class)aClass;
}

當前的isa指向NSObject的Meta Class,所以和NSObject Class不相等。所以輸出結果為NO。
第三題解析如下:
結果是輸出
2017-07-12 10:20:51.067 test[1038:39336] IMP: - [NSObject(Sark) foo] 2017-07-12 10:20:51.068 test[1038:39336] IMP: - [NSObject(Sark) foo]
注意這里有點蹊蹺的是如果將這個分類寫的一個文件import進來會編譯不過的,但是如果直接寫在.m文件,像下面這樣,就是好的,可以編譯過。

#import "ViewController.h"
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo
{
    NSLog(@"IMP: - [NSObject(Sark) foo]");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [NSObject foo];
    [[NSObject new] foo];
}
@end

1)objc runtime加載完后,NSObject的Sark Category被加載。而NSObject的Sark Category的頭文件+ (void)foo并沒有實質參與到工作中,只是給編譯器進行靜態檢查,所以我們編譯上述代碼會出現警告,提示我們沒有實現+ (void)foo方法。而在代碼編譯中,它已經被注釋掉了。
2)實際被加入到Class的method list的方法是- (void)foo,它是一個實例方法,所以加入到當前對象NSObject的方法列表中,而不是NSObject Meta class的方法列表中。
3)當執行[NSObject foo]時,我們看下整個objc_msgSend的過程:
objc_msgSend第一個參數是"(id)objc_getClass("NSObject")",獲得NSObject Class的對象。
類方法在Meta Class的方法列表中找,我們在load Category方法時加入的是- (void)foo實例方法,所以并不在NSObject Meta Class的方法列表中,繼續往super class中找,NSObject Meta Class的super class是NSObject本身,所以,這個時候我們能夠找到 - (void)foo這個方法。所以輸出結果。
當執行[[NSObject new] foo],我們看下整個objc_msgSend的過程:
[NSObject new]生成一個NSObject對象,直接在該對象的類(NSObject)的方法列表里找,能夠找到,所以正常輸出結果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,789評論 0 9
  • Objective-C語言是一門動態語言,他將很多靜態語言在編譯和鏈接時期做的事情放到了運行時來處理。這種動態語言...
    tigger丨閱讀 1,445評論 0 8
  • 原文出處:南峰子的技術博客 Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了...
    _燴面_閱讀 1,271評論 1 5
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,233評論 0 7
  • Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。這種動態語言的...
    有一種再見叫青春閱讀 612評論 0 3