runtime中元類的理解

參考文章

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

元類

這幾天看了一些runtime底層的一些介紹,感受最深的就是對元類的講解了烫幕。突然有種恍然大悟的感覺芒填,以前我們經(jīng)常說一切皆對象梯捕,在這里體現(xiàn)出來了。

1. 元類是什么

眾所周知Objective-C(以下簡稱OC)中的消息機(jī)制雹姊,消息的接收者可以是一個對象瘟忱,也可以是一個類。那么這兩種情況統(tǒng)一為一種情況不是更方便嗎愉昆?蘋果當(dāng)然早就想到了,這也正是元類的用處麻蹋。蘋果統(tǒng)一把消息接收者作為對象跛溉。等等,這也是說類也是對象扮授?yes芳室,就是這樣。就是說刹勃,OC中所有的類都是一種對象堪侯。由一個類實例化來的對象叫實例對象,這好理解荔仁,那么類作為對象(稱之為類對象)伍宦,又是什么類的對象呢芽死?當(dāng)然也容易猜到,就是元類(MetaClass)”⒅簦現(xiàn)在到給元類下定義的時候了:元類就是類對象所屬的類收奔。所以掌呜,實例是類的實例滓玖,類作為對象又是元類的實例。已經(jīng)說了质蕉,OC中所有的類都是一種對象势篡,所以元類也是對象,那么元類是什么的實例呢模暗?答曰:根元類禁悠,同時根元類是其自身的實例。

上面講到了實例對象兑宇、類對象碍侦、元類對象,有什么區(qū)別隶糕?
實例對象:當(dāng)我們在代碼中new一個實例對象時瓷产,拷貝了實例所屬的類的成員變量,但不拷貝類定義的方法枚驻,調(diào)用實例方法時濒旦,調(diào)用實例的isa指針去尋找方法對應(yīng)的函數(shù)指針。
類對象:是一個功能完整的對象再登,特殊之處在于它們是由程序員定義而在運行時由編譯器創(chuàng)建的尔邓,它沒有自己的實例變量(這里區(qū)別于類的成員變量,它們是屬于實例對象的锉矢,而不是屬于類對象的梯嗽,類方法是屬于類對象自己的),但類對象中存著成員變量和實例方法列表沽损。
元類對象:OC的類方法是使用元類的根本原因慷荔,因為其中存儲著對應(yīng)的類對象調(diào)用的方法即類方法。其他時候都傾向于隱藏元類缠俺,因此真實世界沒有人發(fā)送消息給元類對象显晶。元類的定義和創(chuàng)建看起來都是編譯器自動完成的,無須人為干涉壹士。要獲取一個類的元類磷雇,可使用如下定義的函數(shù):

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

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

Class object_getClass(id obj);

由于類對象是元類的實例,所以當(dāng)傳入的參數(shù)為類名時躏救,返回的就是指向該類所屬元類的指針唯笙。

2. 元類的構(gòu)建機(jī)制

既然所有的類都是對象螟蒸,那么元類又是什么類的對象?這樣下去崩掘,不是子子孫孫無窮盡也七嫌?當(dāng)然不行,OC作為一門編程語言苞慢,當(dāng)然要滿足完備性----既定的各條規(guī)則都要滿足诵原,不能相互矛盾,也不能存在漏洞挽放。
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是指向另一個類的指針蚯妇,說白了就是表面這個類是哪個類的實例,以便找到代碼中調(diào)用的本類或父類的類方法暂筝。對于NSObject及其子類箩言,指向的就是它的元類,正如實例中也有個isa指針指向其所屬的類一樣乖杠。而對于元類分扎,每個元類的isa指針都指向根元類。那么根元類的isa指向哪里胧洒?---它自己畏吓。這樣就構(gòu)成了一個封閉的循環(huán),實現(xiàn)了無懈可擊的OC類系統(tǒng)卫漫。這種關(guān)系在下面的圖中有清晰的體現(xiàn)菲饼。

除了isa聲明了實例與所屬類的關(guān)系,還有super_class聲明了類列赎、元類的繼承關(guān)系宏悦。每個類對象都有對應(yīng)的元類,每個類(根類除外)都有一個superclass包吝,同樣每個元類也有一個superclass饼煞,并且子類與子元類、父類與父元類分別在同一個層次诗越。這種關(guān)系借用網(wǎng)上的一張圖來說明砖瞧,一目了然。



注意:根元類的superclass不是nil而是根類嚷狞。對于OC原生的類块促,根元類的父類就是系統(tǒng)的根類NSObject荣堰。但根類不一定是NSObject,因為后面介紹的objc_allocateClassPair函數(shù)也可以創(chuàng)建出一個根類竭翠。

3. 元類的構(gòu)建機(jī)制

上面講到了OC運行時類的定義振坚,這里可以看看對象的定義,重點關(guān)注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,指向?qū)嵗龑ο笏鶎俚念惾焓怠T谶\行時呀狼,[obj aMessage];被轉(zhuǎn)化為objc_msgSend(obj, @selector(aMessage));裂允,這里损离,@selector(aMessage)返回一個SEL數(shù)據(jù)類型,即方法選擇器绝编。SEL主要作用是快速的通過方法名字(aMessage)查找到對應(yīng)方法的函數(shù)指針僻澎,然后調(diào)用其函數(shù)。SEL其本身是一個int型的地址十饥,地址中存放著方法的名字窟勃。在一個類中,每一個方法對應(yīng)著一個SEL逗堵。iOS類中不能存在兩個名稱相同的方法秉氧,即使參數(shù)類型不同,因為SEL是根據(jù)方法名生成蜒秤,相同的方法名稱只能對應(yīng)一個SEL汁咏。

當(dāng)一個消息發(fā)送給任何一個對象,方法的檢查器從對象的isa指針開始作媚,然后是父類攘滩。具體地,在objc_msgSend函數(shù)中纸泡,首先通過obj的isa指針找到obj對應(yīng)的class漂问。在class中,有一塊最近調(diào)用的方法的指針緩存女揭,所以先去cache通過selector查找對應(yīng)的method蚤假,若cache中未找到,再去method list中查找吧兔,若method list中未找到磷仰,則去superClass中查找。若能找到掩驱,則將method加入到cache中芒划,以方便下次查找冬竟,并通過method中的函數(shù)指針跳轉(zhuǎn)到對應(yīng)的函數(shù)中去執(zhí)行。對最后一句不了解的話民逼,請看objc_method定義:

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

由上看出泵殴,OC中實例方法是通過isa找到object所屬的class,再在class中找到要調(diào)用的method拼苍。此可得出結(jié)論:class即類對象中存儲著實例方法笑诅。實際上,類對象中存儲著類定義的一切:成員變量疮鲫、屬性列表吆你、遵守的協(xié)議等,但不包括類方法俊犯。類方法的定義在哪妇多?就是在元類里面。此外元類中還存在著類的信息(類的版本燕侠,名字)者祖,比如發(fā)送一個類消息[class aMessage];,class中的isa就指向class的元類绢彤,在元類中搜索調(diào)用的類方法七问,搜索層次類似于實例方法的搜索。

4. 元類的應(yīng)用

類對象和元類對象的相關(guān)方法:

  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可在運行時創(chuàng)建新的類與元類對籍琳,使用class_addMethod和class_addIvar可向類中增加方法和實例變量,最后使用objc_registerClassPair注冊后贷祈,就可以使用此類了趋急。這體現(xiàn)了OC作為運行時語言的強大之一:在代碼中動態(tài)創(chuàng)建類并添加方法。
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函數(shù)的作用是創(chuàng)建一個新類newClass及其元類势誊,三個參數(shù)依次為newClass的父類呜达,newClass的名稱,第三個參數(shù)通常為0粟耻。然后可向newClass中添加變量及方法查近,注意若要添加類方法眉踱,需用objc_getClass(newClass)獲取元類,然后向元類中添加類方法霜威。接下來必須把newClass注冊到運行時系統(tǒng)谈喳,否則系統(tǒng)不能識別這個類。

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

其中_object_getClass(obj)是一個靜態(tài)內(nèi)聯(lián)函數(shù),代碼實現(xiàn)如下:

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函數(shù)就是返回對象的isa指針大猛。
[obj class]的代碼實現(xiàn)分為兩種情況扭倾,分別是obj為實例對象和類對象,代碼如下所示:

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

通過以上代碼可以看出膛壹,調(diào)用[obj class],不管obj對實例對象還是類對象琼牧,結(jié)果都是一樣的恢筝。

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



    第一題解析如下:
    在調(diào)用[self class]時哀卫,會轉(zhuǎn)化為objc_msgSend函數(shù)巨坊。函數(shù)定義如下:

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

我們把self做為第一個參數(shù)傳遞進(jìn)去。而在調(diào)用[super class]時此改,會轉(zhuǎn)化為objc_msgSendSuper函數(shù)趾撵。看下函數(shù)定義:

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

第一個參數(shù)是objc_super這樣一個結(jié)構(gòu)體共啃,其定義如下:

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

結(jié)構(gòu)體有兩個成員占调,第一個成員是receiver,類似于上面的objc_msgSend函數(shù)第一個參數(shù)self移剪。第二個成員是記錄當(dāng)前類的父類是什么究珊。
所以當(dāng)調(diào)用[self class]時,實際先調(diào)用的是objc_msgSend函數(shù)纵苛,第一個參數(shù)是Son當(dāng)前的這個實例剿涮,然后在Son這個類里面去找-(Class)class這個方法,沒有就去父類Father里找攻人,也沒有取试,最后在NSObject類中發(fā)現(xiàn)這個方法。而-(Class)class的實現(xiàn)就是返回self的類別怀吻,故上述輸出結(jié)果為Son瞬浓。
objc Runtime開源代碼對- (Class)class方法的實現(xiàn):

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

而當(dāng)調(diào)用[super class]時,會轉(zhuǎn)換為objc_msgSendSuper函數(shù)蓬坡。第一步先構(gòu)造objc_super結(jié)構(gòu)體猿棉,結(jié)構(gòu)體第一個成員就是self磅叛,第二個成員是(id)class_getSuperclass(objc_getClass("son")),實際該函數(shù)輸出結(jié)果為Father萨赁。第二部是去Father這個類里去找- (Class)class宪躯,沒有,然后去NSObject類去找位迂。找到了访雪,最后內(nèi)部是使用objc_msgSend(objc_super->receiver, @selector(class))去調(diào)用掂林,此時已經(jīng)和[self class]調(diào)用相同臣缀,故上述輸出結(jié)果仍然返回Son。
第二題解析如下:
運行結(jié)果是
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0
對于
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject calss]];
首先還是看看isKindOfClass的實現(xiàn):

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

當(dāng)[NSObject class]對象第一次進(jìn)行比較的時候泻帮,得到它的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:的實現(xiàn)吧元莫,

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

當(dāng)前的isa指向NSObject的Meta Class赖阻,所以和NSObject Class不相等。所以輸出結(jié)果為NO踱蠢。
第三題解析如下:
結(jié)果是輸出
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進(jìn)來會編譯不過的火欧,但是如果直接寫在.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并沒有實質(zhì)參與到工作中,只是給編譯器進(jìn)行靜態(tài)檢查撕攒,所以我們編譯上述代碼會出現(xiàn)警告陡鹃,提示我們沒有實現(xiàn)+ (void)foo方法。而在代碼編譯中打却,它已經(jīng)被注釋掉了杉适。
2)實際被加入到Class的method list的方法是- (void)foo,它是一個實例方法柳击,所以加入到當(dāng)前對象NSObject的方法列表中猿推,而不是NSObject Meta class的方法列表中。
3)當(dāng)執(zhí)行[NSObject foo]時,我們看下整個objc_msgSend的過程:
objc_msgSend第一個參數(shù)是"(id)objc_getClass("NSObject")"蹬叭,獲得NSObject Class的對象藕咏。
類方法在Meta Class的方法列表中找,我們在load Category方法時加入的是- (void)foo實例方法秽五,所以并不在NSObject Meta Class的方法列表中孽查,繼續(xù)往super class中找,NSObject Meta Class的super class是NSObject本身坦喘,所以盲再,這個時候我們能夠找到 - (void)foo這個方法。所以輸出結(jié)果瓣铣。
當(dāng)執(zhí)行[[NSObject new] foo]答朋,我們看下整個objc_msgSend的過程:
[NSObject new]生成一個NSObject對象,直接在該對象的類(NSObject)的方法列表里找棠笑,能夠找到梦碗,所以正常輸出結(jié)果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蓖救,一起剝皮案震驚了整個濱河市洪规,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌循捺,老刑警劉巖斩例,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異巨柒,居然都是意外死亡樱拴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門洋满,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人珍坊,你說我怎么就攤上這事牺勾。” “怎么了阵漏?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵驻民,是天一觀的道長。 經(jīng)常有香客問我履怯,道長回还,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任叹洲,我火速辦了婚禮柠硕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己蝗柔,他們只是感情好闻葵,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著癣丧,像睡著了一般槽畔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胁编,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天厢钧,我揣著相機(jī)與錄音,去河邊找鬼嬉橙。 笑死坏快,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的憎夷。 我是一名探鬼主播莽鸿,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拾给!你這毒婦竟也來了祥得?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蒋得,失蹤者是張志新(化名)和其女友劉穎级及,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體额衙,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡饮焦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了窍侧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片县踢。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖伟件,靈堂內(nèi)的尸體忽然破棺而出硼啤,到底是詐尸還是另有隱情,我是刑警寧澤斧账,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布谴返,位于F島的核電站,受9級特大地震影響咧织,放射性物質(zhì)發(fā)生泄漏嗓袱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一习绢、第九天 我趴在偏房一處隱蔽的房頂上張望渠抹。 院中可真熱鬧,春花似錦、人聲如沸逼肯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篮幢。三九已至大刊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工盒让, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伴郁。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像蛋叼,于是被迫代替她去往敵國和親焊傅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉狈涮,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,719評論 0 9
  • Objective-C語言是一門動態(tài)語言狐胎,他將很多靜態(tài)語言在編譯和鏈接時期做的事情放到了運行時來處理。這種動態(tài)語言...
    tigger丨閱讀 1,404評論 0 8
  • 原文出處:南峰子的技術(shù)博客 Objective-C語言是一門動態(tài)語言歌馍,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了...
    _燴面_閱讀 1,231評論 1 5
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言握巢,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,195評論 0 7
  • Objective-C語言是一門動態(tài)語言松却,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了運行時來處理暴浦。這種動態(tài)語言的...
    有一種再見叫青春閱讀 585評論 0 3