格物致知iOS類與對(duì)象

欲誠其意者,先致其知;致知在格物。物格而后知至,知至而后意誠。現(xiàn)代漢語詞典中將格物致知解釋為: "推究事物的原理,從而獲得知識(shí)"。

在編程中我們接觸最多的也是最基本的就是類和對(duì)象,當(dāng)我們?cè)趧?chuàng)建類或者實(shí)例化對(duì)象時(shí),是否考慮過類和對(duì)象到底是什么?理解其本質(zhì)才能真正掌握一門語言。本文將從結(jié)構(gòu)類型角度并結(jié)合實(shí)際應(yīng)用探討下Objective-C的類和對(duì)象。

在Objective-C中,對(duì)象是廣義的概念,類也是對(duì)象,所以嚴(yán)謹(jǐn)?shù)恼f法應(yīng)該是類對(duì)象和實(shí)例對(duì)象。既然實(shí)例對(duì)象所屬的類稱為類對(duì)象,那類對(duì)象有所屬的類嗎?有,稱之為元類(Metaclass)。

類對(duì)象

類對(duì)象(Class)是由程序員定義并在運(yùn)行時(shí)由編譯器創(chuàng)建的,它沒有自己的實(shí)例變量,這里需要注意的是類的成員變量和實(shí)例方法列表是屬于實(shí)例對(duì)象的,但其存儲(chǔ)于類對(duì)象當(dāng)中的。我們?cè)?code>/usr/include/objc/objc.h下看看Class的定義:

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

可以看到類是由Class類型來表示的,它是一個(gè)objc_class結(jié)構(gòu)類型的指針。我們接著來看objc_class結(jié)構(gòu)體的定義:

struct objc_class {
    Class                      isa;           // 指向所屬類的指針(_Nonnull)
    Class                      super_class;   // 父類                  
    const char                *name;          // 類名(_Nonnull)
    long                       version;       // 類的版本信息(默認(rèn)為0)
    long                       info;          // 類信息(供運(yùn)行期使用的一些位標(biāo)識(shí))
    long                       instance_size; // 該類的實(shí)例變量大小
    struct objc_ivar_list     *ivars;         // 該類的成員變量鏈表
    struct objc_method_list * *methodLists;   // 方法定義的鏈表
    struct objc_cache         *cache;         // 方法緩存
    struct objc_protocol_list *protocols;     // 協(xié)議鏈表
};
  • isa指針是和Class同類型的objc_class結(jié)構(gòu)指針,類對(duì)象的指針指向其所屬的類,即元類。元類中存儲(chǔ)著類對(duì)象的類方法,當(dāng)訪問某個(gè)類的類方法時(shí)會(huì)通過該isa指針從元類中尋找方法對(duì)應(yīng)的函數(shù)指針

  • super_class為該類所繼承的父類對(duì)象,如果該類已經(jīng)是最頂層的根類(如NSObjectNSProxy), 則 super_class為NULL

  • ivars是一個(gè)指向objc_ivar_list類型的指針,用來存儲(chǔ)每一個(gè)實(shí)例變量的地址

  • info為運(yùn)行期使用的一些位標(biāo)識(shí),比如:
    CLS_CLASS (0x1L)表示該類為普通類, CLS_META (0x2L)則表示該類為元類

  • methodLists用來存放方法列表,根據(jù)info中的標(biāo)識(shí)信息,當(dāng)該類為普通類時(shí),存儲(chǔ)的方法為實(shí)例方法;如果是元類則存儲(chǔ)的類方法

  • cache用于緩存最近使用的方法。系統(tǒng)在調(diào)用方法時(shí)會(huì)先去cache中查找,在沒有查找到時(shí)才會(huì)去methodLists中遍歷獲取需要的方法

實(shí)例對(duì)象

實(shí)例對(duì)象是我們對(duì)類對(duì)象alloc或者new操作時(shí)所創(chuàng)建的,在這個(gè)過程中會(huì)拷貝實(shí)例所屬的類的成員變量,但并不拷貝類定義的方法。調(diào)用實(shí)例方法時(shí),系統(tǒng)會(huì)根據(jù)實(shí)例的isa指針去類的方法列表及父類的方法列表中尋找與消息對(duì)應(yīng)的selector指向的方法。同樣的,我們也來看下其定義:

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

可以看到,這個(gè)結(jié)構(gòu)體只有一個(gè)isa變量,指向?qū)嵗龑?duì)象所屬的類。任何帶有以指針開始并指向類結(jié)構(gòu)的結(jié)構(gòu)都可以被視作objc_object, 對(duì)象最重要的特點(diǎn)是可以給其發(fā)送消息. NSObject類的allocallocWithZone:方法使用函數(shù)class_createInstance來創(chuàng)建objc_object數(shù)據(jù)結(jié)構(gòu)。

另外我們常見的id類型,它是一個(gè)objc_object結(jié)構(gòu)類型的指針。該類型的對(duì)象可以轉(zhuǎn)換為任何一種對(duì)象,類似于C語言中void *指針類型的作用。其定義如下所示:

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

元類對(duì)象

元類(Metaclass)就是類對(duì)象的類,每個(gè)類都有自己的元類,也就是objc_class結(jié)構(gòu)體里面isa指針?biāo)赶虻念? Objective-C的類方法是使用元類的根本原因,因?yàn)槠渲写鎯?chǔ)著對(duì)應(yīng)的類對(duì)象調(diào)用的方法即類方法。

類存儲(chǔ)示意圖.png

所以由上圖可以看到,在給實(shí)例對(duì)象或類對(duì)象發(fā)送消息時(shí),尋找方法列表的規(guī)則為:

  • 當(dāng)發(fā)送消息給實(shí)例對(duì)象時(shí),消息是在尋找這個(gè)對(duì)象的類的方法列表(實(shí)例方法)
  • 當(dāng)發(fā)送消息給類對(duì)象時(shí),消息是在尋找這個(gè)類的元類的方法列表(類方法)

元類,就像之前的類一樣,它也是一個(gè)對(duì)象,也可以調(diào)用它的方法。所以這就意味著它必須也有一個(gè)類。所有的元類都使用根元類作為他們的類。比如所有NSObject的子類的元類都會(huì)以NSObject的元類作為他們的類。

根據(jù)這個(gè)規(guī)則,所有的元類使用根元類作為他們的類,根元類的元類則就是它自己。也就是說基類的元類的isa指針指向他自己。


我們可以通過代碼來實(shí)際驗(yàn)證下, Runtime提供了object_getClass函數(shù):

Class _Nullable object_getClass(id _Nullable obj) 

來獲取對(duì)象所屬的類,看到這個(gè)函數(shù)你也許會(huì)好奇這個(gè)和我們平常接觸的NSObject的[obj class]有什么區(qū)別?

// NSObject.h
- (Class)class;
+ (Class)class;

我們繼續(xù)從runtime的源碼里面尋找答案:

Class object_getClass(id obj) {
    return _object_getClass(obj);
}

object_getClass實(shí)際調(diào)用的是_object_getClass函數(shù),我們接著看其實(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ù)就是返回對(duì)象的isa指針,也就是返回該對(duì)象所指向的所屬類。我們接著看[obj class]的具體實(shí)現(xiàn)(包括類方法和實(shí)例方法兩種):

+ (Class)class {
    return self; // 返回自身指針
}

- (Class)class {
    return object_getClass(self); // 調(diào)用'object_getClass'返回isa指針
}

從代碼中可以看出+ (Class)class返回的是其本身,而- (Class)class則等價(jià)于object_getClass函數(shù)。

我們來寫個(gè)測(cè)試代碼,看看這些函數(shù)的實(shí)際返回值是否和上面的所述保持一致,比如我們有個(gè)RJObject繼承與NSObject:

RJObject *obj = [RJObject new];

Class clsClass0 = [RJObject class];     // 返回RJObject類對(duì)象的本身的地址
Class objClass0 = [obj class];          // isa指向的RJObject類對(duì)象的地址
Class ogcClass0 = object_getClass(obj); // isa指向的RJObject類對(duì)象的地址

NSLog(@"clsClass0 -> %p", clsClass0); // -> 0x10fb22068
NSLog(@"objClass0 -> %p", objClass0); // -> 0x10fb22068
NSLog(@"ogcClass0 -> %p", ogcClass0); // -> 0x10fb22068

打印結(jié)果可以看出,當(dāng)obj為實(shí)例變量時(shí), object_getClass(obj)[obj class]輸出結(jié)果一致,均返回該對(duì)象的isa指針,即指向RJObject類對(duì)象的指針。而[RJObject class]則直接返回RJObject類對(duì)象本身的地址,所以與前面兩者返回的地址相同。

// 'objClass0'為RJObject類對(duì)象(RJObject Class)
Class objClass1 = [objClass0 class];          // 返回RJObject類對(duì)象本身的地址
Class ogcClass1 = object_getClass(objClass0); // isa指向的RJObject元類的地址

NSLog(@"objClass1 -> %p", objClass1); // -> 0x10fb22068
NSLog(@"ogcClass1 -> %p", ogcClass1); // -> 0x10fb22040

此時(shí)objClass0為RJObject的類對(duì)象,所以類方法[objClass0 class]返回的objClass1self, 即RJObject類對(duì)象本身的地址,故結(jié)果與上面的地址相同。而ogcClass1返回的為RJObject元類的地址。

// 'ogcClass1'為RJObject的元類(RJObject metaClass)
Class objClass2 = [ogcClass1 class];          // 返回RJObject元類對(duì)象的本身的地址
Class ogcClass2 = object_getClass(ogcClass1); // isa指向的RJObject元類的元類地址

NSLog(@"objClass2 -> %p", objClass2); // -> 0x10fb22040
NSLog(@"ogcClass2 -> %p", ogcClass2); // -> 0x110ad9e58

同理,這邊ogcClass2為RJObject元類的元類的地址,那問題來了,某個(gè)類它的元類的元類的是什么類呢?這樣下去豈不是元類無窮盡了?擒賊先擒王,我們先來看看根類NSObject的元類和它元類的元類分別是什么:

Class rootMetaCls0 = object_getClass([NSObject class]); // 返回NSObject元類(根元類)的地址
Class rootMetaCls1 = object_getClass(rootMetaCls0);     // 返回NSObject元類(根元類)的元類地址

NSLog(@"rootMetaCls0 -> %p", rootMetaCls0); // -> 0x110ad9e58
NSLog(@"rootMetaCls1 -> %p", rootMetaCls1); // -> 0x110ad9e58

看到結(jié)果就一目了然了,根元類的isa指針指向自己,也就是根元類的元類即其本身。另外,可以發(fā)現(xiàn)ogcClass2的地址和根元類isa的地址相同,說明任意元類的isa指針都指向根元類,這樣就構(gòu)成一個(gè)封閉的循環(huán)。

另外,我們可以通過class_isMetaClass函數(shù)來判斷某個(gè)類是否是元類,比如:

NSLog(@"ogcClass0 is metaClass: %@", class_isMetaClass(objClass0) ? @"YES" : @"NO");
NSLog(@"ogcClass1 is metaClass: %@", class_isMetaClass(ogcClass1) ? @"YES" : @"NO");

輸出結(jié)果為:

LearningClass[58516:3424874] ogcClass0 is metaClass: NO
LearningClass[58516:3424874] ogcClass1 is metaClass: YES

日志表明ogcClass0為類對(duì)象,而ogcClass1則為元類對(duì)象,這與我們上面的分析是一致的。

類和元類的父類指向情況也可以參照上面的步驟,通過class_getSuperclass或者[obj superClass]函數(shù)來獲取分析,這邊就不再贅述了。


除了isa聲明了實(shí)例與所屬類的關(guān)系,還有superClass表明了類和元類的繼承關(guān)系,類對(duì)象和元類對(duì)象都有父類。同樣,為了形成一個(gè)閉環(huán),根類的父類為nil, 根元類的父類則指向其根類。我們可以通過一張示意圖來看下三種對(duì)象之間的連接關(guān)系:

類關(guān)系示意圖

總結(jié)一下實(shí)例對(duì)象,類對(duì)象以及元類對(duì)象之間的isa指向和繼承關(guān)系的規(guī)則為:

規(guī)則一: 實(shí)例對(duì)象的isa指向該類,類的isa指向元類(metaClass)

規(guī)則二: 類的superClass指向其父類,如果該類為根類則值為nil

規(guī)則三: 元類的isa指向根元類,如果該元類是根元類則指向自身

規(guī)則四: 元類的superClass指向父元類,若根元類則指向該根類

動(dòng)態(tài)創(chuàng)建類

Objective-C作為動(dòng)態(tài)語言的優(yōu)勢(shì)在于它能在運(yùn)行時(shí)創(chuàng)建類和對(duì)象,并向類中增加方法和實(shí)例變量。具體示例如下:

Class newClass = objc_allocateClassPair([NSObject class], "RJInfo", 0);

if (!class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:")) {
    NSLog(@"Add method 'report' failed!");
}
if (!class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *))) {
    NSLog(@"Add ivar '_name' failed!");
}

objc_registerClassPair(newClass);

上面代碼創(chuàng)建了一個(gè)RJInfo的類,并分別添加了_name成員變量和report實(shí)例方法。需要注意的是,方法和變量必須在objc_allocateClassPairobjc_registerClassPair之間進(jìn)行添加。所以,在運(yùn)行時(shí)創(chuàng)建一個(gè)類只需要3個(gè)步驟:

首先是調(diào)用objc_allocateClassPair為新建的類分配內(nèi)存,三個(gè)參數(shù)依次為newClass的父類,newClass的名稱,第三個(gè)參數(shù)通常為0, 從這個(gè)函數(shù)名字可以看到新建的類是一個(gè)pair, 也就是成對(duì)的類,那為什么新建一個(gè)類會(huì)出現(xiàn)一對(duì)類呢?是的,元類!類和元類是成對(duì)出現(xiàn)的,每個(gè)類都有自己所屬的元類,所以新建一個(gè)類需要同時(shí)創(chuàng)建類以及它的元類。

然后就可以向newClass中添加變量及方法了,注意若要添加類方法,需用objc_getClass(newClass)獲取元類,然后向元類中添加類方法。因?yàn)槭纠椒ㄊ谴鎯?chǔ)在類中的,而類方法則是存儲(chǔ)在元類中。最后必須把newClass注冊(cè)到運(yùn)行時(shí)系統(tǒng),否則系統(tǒng)是不能識(shí)別這個(gè)類的。

上面的代碼中添加了一個(gè)成員變量_name, 我們來看下實(shí)際應(yīng)用中如何獲取和使用這個(gè)變量:

unsigned int varCount;

Ivar *varList = class_copyIvarList(newClass, &varCount);

for (int i = 0; i < varCount; i++) {
    NSLog(@"var name: %s", ivar_getName(varList[i]));
}

free(varList);

id infoInstance = [[newClass alloc] init];
Ivar nameIvar   = class_getInstanceVariable(newClass, "_name");

object_setIvar(infoInstance, nameIvar, @"Ryan Jin");

NSLog(@"var value: %@",object_getIvar(infoInstance, nameIvar));

我們可以通過class_copyIvarList來查看實(shí)例變量列表,注意獲取的varList列表需要調(diào)用free()函數(shù)釋放。當(dāng)前只添加了一個(gè)變量,所以varCount1, 在調(diào)用ivar_getName打印出變量的名字。如若對(duì)_name賦值,則需要先實(shí)例化newClass對(duì)象,并取出對(duì)象的該變量后調(diào)用object_setIvar進(jìn)行賦值操作。示例代碼的輸出結(jié)果為:

LearningClass[58516:3424874] var name: _name
LearningClass[58516:3424874] var value: Ryan Jin

好了,驗(yàn)證完變量的添加,繼續(xù)看方法的添加和使用。上文的示例中添加了report方法,但僅僅是做了SEL方法名的聲明,我們來接著完成其IMP所指向函數(shù)ReportFunction的具體實(shí)現(xiàn):

void ReportFunction(id self, SEL _cmd) {
    Class currentClass = [self class];
    Class metaClass    = objc_getMetaClass(class_getName(currentClass));
    
    NSLog(@"Class is %@, and super - %@.", currentClass, [self superclass]);
    NSLog(@"%@'s meta class is %p.", NSStringFromClass(currentClass), metaClass);
}

在函數(shù)實(shí)現(xiàn)中我們打印了類,父類以及元類的相關(guān)信息,為了運(yùn)行ReportFunction, 我們需要?jiǎng)?chuàng)建一個(gè)動(dòng)態(tài)實(shí)例來創(chuàng)建類的實(shí)例對(duì)象并調(diào)用report方法:

id instanceOfNewClass = [[newClass alloc] init];
    
[instanceOfNewClass performSelector:@selector(report)];

輸出結(jié)果:

LearningClass[58516:3424874] Class is RJInfo, and super - NSObject.
LearningClass[58516:3424874] RJInfo's meta class is 0x600000253920.

除了給類添加方法,我們同樣也可以動(dòng)態(tài)修改已存在方法的實(shí)現(xiàn),比如:

class_replaceMethod(newClass, @selector(report), (IMP)ReportReplacedFunction, "v@:");

這樣就將report這個(gè)SEL所指向的IMP實(shí)現(xiàn)換成了ReportReplacedFunction. 如果類中不存在name指定的方法, class_replaceMethod則類似于class_addMethod函數(shù)一樣會(huì)添加方法;如果類中已存在name指定的方法,則類似于method_setImplementation一樣替代原方法的實(shí)現(xiàn)。

看到class_replaceMethod的解釋,相信你已經(jīng)發(fā)現(xiàn)了,這不就是Method Swizzling嗎?沒錯(cuò),所謂的黑魔法,其實(shí)就是底層原理的應(yīng)用而已!

本質(zhì)探究

知其然亦知其所以然才是獲取知識(shí)的正確方式,理解了類和對(duì)象的本質(zhì)后,我們來看看格物致知后的理論可以引導(dǎo)出哪些應(yīng)用和認(rèn)識(shí):

屬性

在Objective-C中,屬性(property)和成員變量是不同的。那么,屬性的本質(zhì)是什么?它和成員變量之間有什么區(qū)別?簡(jiǎn)單來說屬性是添加了存取方法的成員變量,也就是:

@property = ivar + getter + setter;

因此,我們每定義一個(gè)@property都會(huì)添加對(duì)應(yīng)的ivar, gettersetter到類結(jié)構(gòu)體objc_class中。具體來說,系統(tǒng)會(huì)在objc_ivar_list中添加一個(gè)成員變量的描述,然后在methodLists中分別添加settergetter方法的描述。

方法調(diào)用

如上文所述,方法調(diào)用是通過查詢對(duì)象的isa指針?biāo)赶驓w屬類中的methodLists來完成。這里我們通過孫源在runtime分享會(huì)上的一道題目來理解下。假設(shè)我們有一個(gè)類RJSark定義如下:

@interface RJSark : NSObject

- (void)speak;

@end

然后通過如下方式調(diào)用speak方法:

@implementation RJViewController

- (void)viewDidLoad 
{
    [super viewDidLoad];
    
    id cls    = [RJSark class];
    void *obj = &cls;
    
    [(__bridge id)obj speak];
}

@end

這里會(huì)正常完成調(diào)用,并不會(huì)導(dǎo)致程序crash. 這又是為什么呢?我們先來看下cls. 顯然,它是RJSark的類對(duì)象,經(jīng)過void *obj = &cls賦值后obj為指向cls的指針,再通過(__bridge id)將其轉(zhuǎn)換為id對(duì)象。上文中我們提到id其實(shí)是一個(gè)objc_object結(jié)構(gòu)體,里面存放了指向所屬類的isa指針,所以調(diào)用[obj speak]能夠找到它的isa所指向的類對(duì)象(也就是RJSark類)的方法列表并完成調(diào)用,但其實(shí)obj并不是RJSark的實(shí)例對(duì)象,它僅僅擁有和RJSark實(shí)例對(duì)象一樣的isa指針而已。

空說無憑,我們將上面的代碼稍微修改后驗(yàn)證下:

id cls       = [RJSark class];
RJSark *sark = [[cls alloc] init];
void *obj    = &cls;

NSLog(@"cls  = %p", cls);
NSLog(@"sark = %p", objc_getClass(object_getClassName(sark)));
NSLog(@"obj  = %p", objc_getClass(object_getClassName((__bridge id)obj)));

輸出結(jié)果為:

LearningClass[58516:3424874] cls  = 0x10fbd02d0
LearningClass[58516:3424874] sark = 0x10fbd02d0
LearningClass[58516:3424874] obj  = 0x10fbd02d0

可以發(fā)現(xiàn)objsark的isa指針?biāo)赶虻牡刂废嗤遗ccls的地址一致,也就是它們都指向cls類對(duì)象。

父類對(duì)象

我們直接來看一個(gè)面試題, Father繼承與NSObject, Son則繼承于Father類,分別調(diào)用[self class][super class], 輸出結(jié)果是?

@implementation Son : Father

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

輸出結(jié)果都為Son, 為什么[super class]的結(jié)果不是Father? 我們簡(jiǎn)單分析下就明白了。實(shí)例對(duì)象的方法列表是存放在isa所指向的類對(duì)象中的,所以調(diào)用[self class]的時(shí)候會(huì)去self的isa所指向的Son類對(duì)象中尋找該方法,在沒有重載[obj class]的情況下, Son類對(duì)象是沒有這個(gè)方法的,此時(shí)會(huì)接著在父類對(duì)象的方法列表中查找,最終會(huì)發(fā)現(xiàn)NSObject存儲(chǔ)了該方法,所以[self class]會(huì)返回實(shí)例對(duì)象(self)所屬的Son這個(gè)類對(duì)象。

[super class]則指定從父類Father的方法列表開始去查找- (Class)class這個(gè)方法,顯然Father沒有這個(gè)方法,最終還是要查找到NSObject類對(duì)象的方法列表中,需要注意的是不管是[self class]還是[super class], 它們都是調(diào)用的實(shí)例對(duì)象的- (Class)class方法,雖然其指向的類對(duì)象不同,但實(shí)例對(duì)象都是self本身,再強(qiáng)調(diào)下區(qū)分開實(shí)例對(duì)象和類對(duì)象!因而返回的也是當(dāng)前self的isa所指向的Son類。

其實(shí)superobjc_super類型的結(jié)構(gòu)體,它包含了當(dāng)前的實(shí)例對(duì)象self以及父類的類對(duì)象。更詳細(xì)的解答可以參考@iOS程序犭袁的博文。


除了用super來指向父類外,我們還可以用isKindOfClassisMemberOfClass來判斷對(duì)象的繼承關(guān)系。這兩個(gè)函數(shù)有什么區(qū)別呢?同樣,先來看一個(gè)測(cè)試題:

BOOL r1 = [[NSObject class] isKindOfClass:[NSObject class]]; // -> YES
BOOL r2 = [[RJObject class] isKindOfClass:[RJObject class]]; // -> NO

BOOL r3 = [[NSObject class] isMemberOfClass:[NSObject class]]; // -> NO
BOOL r4 = [[RJObject class] isMemberOfClass:[RJObject class]]; // -> NO

為什么只有r1YES? 實(shí)際上isKindOfClass是判斷對(duì)象是否為Class的實(shí)例或子類,而isMemberOfClass則是判斷對(duì)象是否為Class的實(shí)例。還是不明白?沒關(guān)系,我們直接來看看這兩個(gè)函數(shù)的源碼實(shí)現(xiàn),看看它們本質(zhì)上是以什么作為判斷標(biāo)準(zhǔn)的:

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

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;  
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls; 
}

注意上面的題目是調(diào)用的類方法,所以我們分析下類方法的實(shí)現(xiàn),至于實(shí)例方法也是類似的。可以看到isMemberOfClass的判斷是先調(diào)用object_getClass獲取isa所指向的歸屬類,也就是元類,然后直接判斷cls是否就是被比較的對(duì)象的元類。而[NSObject class]的元類是根元類,顯然不等于[NSObject class]本身,所以r3返回NO, r4也是同理。

isKindOfClass也是先獲取當(dāng)前對(duì)象的元類,但是會(huì)循環(huán)獲取其isa所指向類的父類進(jìn)行比較,只要該元類或者元類的父類與cls相對(duì)則返回YES. RJObject的元類,以及父元類(最終指向根元類)都不等于RJObject對(duì)象,所以r2返回NO. 那為什么r1返回YES呢?還記得上文所說的閉環(huán)嗎?根元類的父類指向根類本身!顯然, r1符合了isKindOfClass的判斷標(biāo)準(zhǔn)。

學(xué)以致用

到這里理論部分就結(jié)束了。那么,問題來了,理解了類和對(duì)象的本質(zhì)原理有什么實(shí)際應(yīng)用價(jià)值嗎?可以讓我們更優(yōu)雅的解決項(xiàng)目中遇到的問題和需求嗎?Talk is cheap, show me the code:

比如App常見的記錄用戶行為的數(shù)據(jù)統(tǒng)計(jì)需求,俗稱埋點(diǎn)。具體來說假設(shè)我們需要記錄用戶對(duì)按鈕的點(diǎn)擊。通常情況下,我們會(huì)在按鈕的點(diǎn)擊事件里面直接加上數(shù)據(jù)統(tǒng)計(jì)的代碼,但這樣做的問題在于會(huì)對(duì)業(yè)務(wù)代碼進(jìn)行侵入,且統(tǒng)計(jì)的代碼散落各處,難以維護(hù)。

當(dāng)然,我們還可以創(chuàng)建一個(gè)UIButton的子類,在子類中重載點(diǎn)擊事件的響應(yīng)函數(shù),并在其中加上統(tǒng)計(jì)數(shù)據(jù)部分的代碼:

-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event

這樣做是可以的,但是現(xiàn)有工程中所有需要支持?jǐn)?shù)據(jù)統(tǒng)計(jì)的按鈕都必須替換成該子類,而且如果哪天不需要支持埋點(diǎn)功能了并需要遷移復(fù)用業(yè)務(wù)代碼,那還得一個(gè)個(gè)再改回去。所以,我們需要一個(gè)更優(yōu)雅的實(shí)現(xiàn)。

我們可以利用動(dòng)態(tài)創(chuàng)建類并添加方法的思路來實(shí)現(xiàn)這個(gè)需求,這邊只是以埋點(diǎn)作為示例,你也可以利用該思路擴(kuò)展任意需要處理的需求和功能。簡(jiǎn)單來說就是我們創(chuàng)建一個(gè)UIButton的Category, 然后在需要埋點(diǎn)的情況下動(dòng)態(tài)生成一個(gè)新的UIButton子類,并給其添加一個(gè)可以記錄數(shù)據(jù)的事件響應(yīng)方法來替代默認(rèn)的方法,如下所示:

//
//  UIButton+Tracking.m
//  LearningClass
//
//  Created by Ryan Jin on 07/03/2018.
//  Copyright ? 2018 ArcSoft. All rights reserved.
//

#import "UIButton+Tracking.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation UIButton (Tracking)

- (void)enableEventTracking
{
    NSString *className = [NSString stringWithFormat:@"EventTracking_%@",self.class];
    Class kClass        = objc_getClass([className UTF8String]);
    
    if (!kClass) {
        kClass = objc_allocateClassPair([self class], [className UTF8String], 0);
    }
    SEL setterSelector  = NSSelectorFromString(@"sendAction:to:forEvent:");
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    
    object_setClass(self, kClass); // 轉(zhuǎn)換當(dāng)前類從UIButton到新建的EventTracking_UIButton類
    
    const char *types   = method_getTypeEncoding(setterMethod);
    
    class_addMethod(kClass, setterSelector, (IMP)eventTracking_SendAction, types);
    
    objc_registerClassPair(kClass);
}

static void eventTracking_SendAction(id self, SEL _cmd, SEL action ,id target , UIEvent *event) {
    struct objc_super superclass = {
        .receiver    = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    void (*objc_msgSendSuperCasted)(const void *, SEL, SEL, id, UIEvent *) = (void *)objc_msgSendSuper;
    
    // to do event tracking...
    NSLog(@"Click event record: target = %@, action = %@, event = %ld", target, NSStringFromSelector(action), (long)event.type);
    
    objc_msgSendSuperCasted(&superclass, _cmd, action, target, event);
}

@end

然后在添加按鈕的地方,如果需要數(shù)據(jù)統(tǒng)計(jì)功能,則調(diào)用enableEventTracking函數(shù)來內(nèi)嵌打點(diǎn)功能。使用示例如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 50, 30)];
    
    button.layer.borderColor   = [[UIColor redColor] CGColor];
    button.layer.borderWidth   = 1.0f;
    button.layer.cornerRadius  = 4.0f;
    button.layer.masksToBounds = YES;
    
    [button addTarget:self action:@selector(trackingButtonAction:)
                 forControlEvents:UIControlEventTouchUpInside];
    
    [self.view addSubview:button];

    [button enableEventTracking];
}

- (void)trackingButtonAction:(UIButton *)sender
{
    // to do whatever you want...
    NSLog(@"%s", __func__);
}

打印輸出信息為:

LearningClass[58516:3424874] Click event record: target = <ViewController: 0x7f97a5d0cb80>, action = trackingButtonAction:, event = 0
LearningClass[58516:3424874] -[ViewController trackingButtonAction:]

浮于表面探究問題不失為一種方法,但是弄清楚本質(zhì)才是真正意義上的解決疑惑。

參考文章

  1. 清晰理解Objective-C元類
  2. Objective-C Runtime 運(yùn)行時(shí)之一: 類與對(duì)象
  3. What is a meta-class in Objective-C?
  4. 一只iOS魔法師的土系魔法講義
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容