參考文章
清晰理解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. 元類的應用
類對象和元類對象的相關方法:
- object_getClass跟隨實例的isa指針,返回此實例所屬的類,對于實例對象(instance)返回的是類(class),對于類(class)則返回的是元類(metaclass);
- -class方法對于實例對象(instance)會返回類(class),但對于類(class)則不會返回元類(metaclass),而只會返回類本身,即[@"instance" class]返回的是__NSCFConstantString,而[NSString class]返回的是NSString。
- class_isMetaClass可判斷某類是否為元類。
- 使用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注冊到運行時系統,否則系統不能識別這個類。
- 根據以上理解比較一下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對實例對象還是類對象,結果都是一樣的。
-
看下這幾個個面試題吧
第一題解析如下:
在調用[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)的方法列表里找,能夠找到,所以正常輸出結果。