格物致知iOS系列之类与对象

欲诚其意者,先致其知;致知在格物。物格然后知至,知至然后意诚。现代汉语词典中将格物致知解释为: "推究事物的原理,从而得到知识"。html

在编程中咱们接触最多的也是最基本的就是类和对象,当咱们在建立类或者实例化对象时,是否考虑过类和对象究竟是什么?理解其本质才能真正掌握一门语言。本文将从结构类型角度并结合实际应用探讨下Objective-C的类和对象。git

在Objective-C中,对象是广义的概念,类也是对象,因此严谨的说法应该是类对象和实例对象。既然实例对象所属的类称为类对象,那类对象有所属的类吗?有,称之为元类(Metaclass)。程序员

类对象

类对象(Class)是由程序员定义并在运行时由编译器建立的,它没有本身的实例变量,这里须要注意的是类的成员变量和实例方法列表是属于实例对象的,但其存储于类对象当中的。咱们在/usr/include/objc/objc.h下看看Class的定义:github

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

能够看到类是由Class类型来表示的,它是一个objc_class结构类型的指针。咱们接着来看objc_class结构体的定义:面试

struct objc_class {
    Class                      isa;           // 指向所属类的指针(_Nonnull)
    Class                      super_class;   // 父类 
    const char                *name;          // 类名(_Nonnull)
    long                       version;       // 类的版本信息(默认为0)
    long                       info;          // 类信息(供运行期使用的一些位标识)
    long                       instance_size; // 该类的实例变量大小
    struct objc_ivar_list     *ivars;         // 该类的成员变量链表
    struct objc_method_list * *methodLists;   // 方法定义的链表
    struct objc_cache         *cache;         // 方法缓存
    struct objc_protocol_list *protocols;     // 协议链表
};
复制代码
  • isa指针是和Class同类型的objc_class结构指针,类对象的指针指向其所属的类,即元类。元类中存储着类对象的类方法,当访问某个类的类方法时会经过该isa指针从元类中寻找方法对应的函数指针objective-c

  • super_class为该类所继承的父类对象,若是该类已是最顶层的根类(如NSObjectNSProxy), 则 super_class为NULL编程

  • ivars是一个指向objc_ivar_list类型的指针,用来存储每个实例变量的地址缓存

  • info为运行期使用的一些位标识,好比: CLS_CLASS (0x1L)表示该类为普通类, CLS_META (0x2L)则表示该类为元类bash

  • methodLists用来存放方法列表,根据info中的标识信息,当该类为普通类时,存储的方法为实例方法;若是是元类则存储的类方法数据结构

  • cache用于缓存最近使用的方法。系统在调用方法时会先去cache中查找,在没有查找到时才会去methodLists中遍历获取须要的方法

实例对象

实例对象是咱们对类对象alloc或者new操做时所建立的,在这个过程当中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法。一样的,咱们也来看下其定义:

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

能够看到,这个结构体只有一个isa变量,指向实例对象所属的类。任何带有以指针开始并指向类结构的结构均可以被视做objc_object, 对象最重要的特色是能够给其发送消息. NSObject类的allocallocWithZone:方法使用函数class_createInstance来建立objc_object数据结构。

另外咱们常见的id类型,它是一个objc_object结构类型的指针。该类型的对象能够转换为任何一种对象,相似于C语言中void *指针类型的做用。其定义以下所示:

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

元类对象

元类(Metaclass)就是类对象的类,每一个类都有本身的元类,也就是objc_class结构体里面isa指针所指向的类. Objective-C的类方法是使用元类的根本缘由,由于其中存储着对应的类对象调用的方法即类方法。

类存储示意图.png

因此由上图能够看到,在给实例对象或类对象发送消息时,寻找方法列表的规则为:

  • 当发送消息给实例对象时,消息是在寻找这个对象的类的方法列表(实例方法)
  • 当发送消息给类对象时,消息是在寻找这个类的元类的方法列表(类方法)

元类,就像以前的类同样,它也是一个对象,也能够调用它的方法。因此这就意味着它必须也有一个类。全部的元类都使用根元类做为他们的类。好比全部NSObject的子类的元类都会以NSObject的元类做为他们的类。

根据这个规则,全部的元类使用根元类做为他们的类,根元类的元类则就是它本身。也就是说基类的元类的isa指针指向他本身。


咱们能够经过代码来实际验证下, Runtime提供了object_getClass函数:

Class _Nullable object_getClass(id _Nullable obj) 
复制代码

来获取对象所属的类,看到这个函数你也许会好奇这个和咱们日常接触的NSObject的[obj class]有什么区别?

// NSObject.h
- (Class)class;
+ (Class)class;
复制代码

咱们继续从runtime的源码里面寻找答案:

Class object_getClass(id obj) {
    return _object_getClass(obj);
}
复制代码

object_getClass实际调用的是_object_getClass函数,咱们接着看其实现:

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]的具体实现(包括类方法和实例方法两种):

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

- (Class)class {
    return object_getClass(self); // 调用'object_getClass'返回isa指针
}
复制代码

从代码中能够看出+ (Class)class返回的是其自己,而- (Class)class则等价于object_getClass函数。

咱们来写个测试代码,看看这些函数的实际返回值是否和上面的所述保持一致,好比咱们有个RJObject继承自NSObject:

RJObject *obj = [RJObject new];

Class clsClass0 = [RJObject class];     // 返回RJObject类对象的自己的地址
Class objClass0 = [obj class];          // isa指向的RJObject类对象的地址
Class ogcClass0 = object_getClass(obj); // isa指向的RJObject类对象的地址

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

打印结果能够看出,当obj为实例变量时, object_getClass(obj)[obj class]输出结果一致,均返回该对象的isa指针,即指向RJObject类对象的指针。而[RJObject class]则直接返回RJObject类对象自己的地址,因此与前面二者返回的地址相同。

// 'objClass0'为RJObject类对象(RJObject Class)
Class objClass1 = [objClass0 class];          // 返回RJObject类对象自己的地址
Class ogcClass1 = object_getClass(objClass0); // isa指向的RJObject元类的地址

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

此时objClass0为RJObject的类对象,因此类方法[objClass0 class]返回的objClass1self, 即RJObject类对象自己的地址,故结果与上面的地址相同。而ogcClass1返回的为RJObject元类的地址。

// 'ogcClass1'为RJObject的元类(RJObject metaClass)
Class objClass2 = [ogcClass1 class];          // 返回RJObject元类对象的自己的地址
Class ogcClass2 = object_getClass(ogcClass1); // isa指向的RJObject元类的元类地址

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

同理,这边ogcClass2为RJObject元类的元类的地址,那问题来了,某个类它的元类的元类的是什么类呢?这样下去岂不是元类无穷尽了?擒贼先擒王,咱们先来看看根类NSObject的元类和它元类的元类分别是什么:

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

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

看到结果就一目了然了,根元类的isa指针指向本身,也就是根元类的元类即其自己。另外,能够发现ogcClass2的地址和根元类isa的地址相同,说明任意元类的isa指针都指向根元类,这样就构成一个封闭的循环。

另外,咱们能够经过class_isMetaClass函数来判断某个类是不是元类,好比:

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

输出结果为:

LearningClass[58516:3424874] ogcClass0 is metaClass: NO
LearningClass[58516:3424874] ogcClass1 is metaClass: YES
复制代码

日志代表ogcClass0为类对象,而ogcClass1则为元类对象,这与咱们上面的分析是一致的。

类和元类的父类指向状况也能够参照上面的步骤,经过class_getSuperclass或者[obj superClass]函数来获取分析,这边就再也不赘述了。


除了isa声明了实例与所属类的关系,还有superClass代表了类和元类的继承关系,类对象和元类对象都有父类。一样,为了造成一个闭环,根类的父类为nil, 根元类的父类则指向其根类。咱们能够经过一张示意图来看下三种对象之间的链接关系:

类关系示意图

总结一下实例对象,类对象以及元类对象之间的isa指向和继承关系的规则为:

规则一: 实例对象的isa指向该类,类的isa指向元类(metaClass)

规则二: 类的superClass指向其父类,若是该类为根类则值为nil

规则三: 元类的isa指向根元类,若是该元类是根元类则指向自身

规则四: 元类的superClass指向父元类,若根元类则指向该根类

动态建立类

Objective-C做为动态语言的优点在于它能在运行时建立类和对象,并向类中增长方法和实例变量。具体示例以下:

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);
复制代码

上面代码建立了一个RJInfo的类,并分别添加了_name成员变量和report实例方法。须要注意的是,方法和变量必须在objc_allocateClassPairobjc_registerClassPair之间进行添加。因此,在运行时建立一个类只须要3个步骤:

首先是调用objc_allocateClassPair为新建的类分配内存,三个参数依次为newClass的父类,newClass的名称,第三个参数一般为0, 从这个函数名字能够看到新建的类是一个pair, 也就是成对的类,那为何新建一个类会出现一对类呢?是的,元类!类和元类是成对出现的,每一个类都有本身所属的元类,因此新建一个类须要同时建立类以及它的元类。

而后就能够向newClass中添加变量及方法了,注意若要添加类方法,需用objc_getClass(newClass)获取元类,而后向元类中添加类方法。由于示例方法是存储在类中的,而类方法则是存储在元类中。最后必须把newClass注册到运行时系统,不然系统是不能识别这个类的。

上面的代码中添加了一个成员变量_name, 咱们来看下实际应用中如何获取和使用这个变量:

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来查看实例变量列表,注意获取的varList列表须要调用free()函数释放。当前只添加了一个变量,因此varCount1, 在调用ivar_getName打印出变量的名字。如若对_name赋值,则须要先实例化newClass对象,并取出对象的该变量后调用object_setIvar进行赋值操做。示例代码的输出结果为:

LearningClass[58516:3424874] var name: _name
LearningClass[58516:3424874] var value: Ryan Jin
复制代码

好了,验证完变量的添加,继续看方法的添加和使用。上文的示例中添加了report方法,但仅仅是作了SEL方法名的声明,咱们来接着完成其IMP所指向函数ReportFunction的具体实现:

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);
}
复制代码

在函数实现中咱们打印了类,父类以及元类的相关信息,为了运行ReportFunction, 咱们须要建立一个动态实例来建立类的实例对象并调用report方法:

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

输出结果:

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

除了给类添加方法,咱们一样也能够动态修改已存在方法的实现,好比:

class_replaceMethod(newClass, @selector(report), (IMP)ReportReplacedFunction, "v@:");
复制代码

这样就将report这个SEL所指向的IMP实现换成了ReportReplacedFunction. 若是类中不存在name指定的方法, class_replaceMethod则相似于class_addMethod函数同样会添加方法;若是类中已存在name指定的方法,则相似于method_setImplementation同样替代原方法的实现。

看到class_replaceMethod的解释,相信你已经发现了,这不就是Method Swizzling吗?没错,所谓的黑魔法,其实就是底层原理的应用而已!

本质探究

知其然亦知其因此然才是获取知识的正确方式,理解了类和对象的本质后,咱们来看看格物致知后的理论能够引导出哪些应用和认识:

属性

在Objective-C中,属性(property)和成员变量是不一样的。那么,属性的本质是什么?它和成员变量之间有什么区别?简单来讲属性是添加了存取方法的成员变量,也就是:

@property = ivar + getter + setter;
复制代码

所以,咱们每定义一个@property都会添加对应的ivar, gettersetter到类结构体objc_class中。具体来讲,系统会在objc_ivar_list中添加一个成员变量的描述,而后在methodLists中分别添加settergetter方法的描述。

方法调用

如上文所述,方法调用是经过查询对象的isa指针所指向归属类中的methodLists来完成。这里咱们经过孙源在runtime分享会上的一道题目来理解下。假设咱们有一个类RJSark定义以下:

@interface RJSark : NSObject

- (void)speak;

@end
复制代码

而后经过以下方式调用speak方法:

@implementation RJViewController

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

@end
复制代码

这里会正常完成调用,并不会致使程序crash. 这又是为何呢?咱们先来看下cls. 显然,它是RJSark的类对象,通过void *obj = &cls赋值后obj为指向cls的指针,再经过(__bridge id)将其转换为id对象。上文中咱们提到id实际上是一个objc_object结构体,里面存放了指向所属类的isa指针,因此调用[obj speak]可以找到它的isa所指向的类对象(也就是RJSark类)的方法列表并完成调用,但其实obj并非RJSark的实例对象,它仅仅拥有和RJSark实例对象同样的isa指针而已。

空说无凭,咱们将上面的代码稍微修改后验证下:

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)));
复制代码

输出结果为:

LearningClass[58516:3424874] cls  = 0x10fbd02d0
LearningClass[58516:3424874] sark = 0x10fbd02d0
LearningClass[58516:3424874] obj  = 0x10fbd02d0
复制代码

能够发现objsark的isa指针所指向的地址相同且与cls的地址一致,也就是它们都指向cls类对象。

注意这边用的是objc_getClass方法,该方法只是单纯的返回本类的地址,上文用到的object_getClass方法返回的才是isa指针所指向的(元)类对象地址

父类对象

咱们仍是直接来看一个面试题, Father继承与NSObject, Son则继承于Father类,分别调用[self class][super class], 输出结果是?

@implementation Son : Father

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

输出结果都为Son, 为何[super class]的结果不是Father? 咱们简单分析下就明白了。实例对象的方法列表是存放在isa所指向的类对象中的,因此调用[self class]的时候会去self的isa所指向的Son类对象中寻找该方法,在没有重载[obj class]的状况下, Son类对象是没有这个方法的,此时会接着在父类对象的方法列表中查找,最终会发现NSObject存储了该方法,因此[self class]会返回实例对象(self)所属的Son这个类对象

[super class]则指定从父类Father的方法列表开始去查找- (Class)class这个方法,显然Father没有这个方法,最终仍是要查找到NSObject类对象的方法列表中,须要注意的是不论是[self class]仍是[super class], 它们都是调用的实例对象的- (Class)class方法,虽然其指向的类对象不一样,但实例对象都是self自己,再强调下区分开实例对象和类对象!于是返回的也是当前self的isa所指向的Son类。

其实superobjc_super类型的结构体,它包含了当前的实例对象self以及父类的类对象。更详细的解答能够参考@iOS程序犭袁的博文。


除了用super来指向父类外,咱们还能够用isKindOfClassisMemberOfClass来判断对象的继承关系。这两个函数有什么区别呢?一样,先来看一个测试题:

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? 实际上isKindOfClass是判断对象是否为Class的实例或子类,而isMemberOfClass则是判断对象是否为Class的实例。仍是不明白?不要紧,咱们直接来看看这两个函数的源码实现,看看它们本质上是以什么做为判断标准的:

+ (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; 
}
复制代码

注意上面的题目是调用的类方法,因此咱们分析下类方法的实现,至于实例方法也是相似的。能够看到isMemberOfClass的判断是先调用object_getClass获取isa所指向的归属类,也就是元类,而后直接判断cls是否就是被比较的对象的元类。而[NSObject class]的元类是根元类,显然不等于[NSObject class]自己,因此r3返回NO, r4也是同理。

isKindOfClass也是先获取当前对象的元类,可是会循环获取其isa所指向类的父类进行比较,只要该元类或者元类的父类与cls相对则返回YES. RJObject的元类,以及父元类(最终指向根元类)都不等于RJObject对象,因此r2返回NO. 那为何r1返回YES呢?还记得上文所说的闭环吗?根元类的父类指向根类自己!显然, r1符合了isKindOfClass的判断标准。

学以至用

到这里理论部分就结束了。那么,问题来了,理解了类和对象的本质原理有什么实际应用价值吗?可让咱们更优雅的解决项目中遇到的问题和需求吗?Talk is cheap, show me the code:

好比App常见的记录用户行为的数据统计需求,俗称埋点。具体来讲假设咱们须要记录用户对按钮的点击。一般状况下,咱们会在按钮的点击事件里面直接加上数据统计的代码,但这样作的问题在于会对业务代码进行侵入,且统计的代码散落各处,难以维护。

固然,咱们还能够建立一个UIButton的子类,在子类中重载点击事件的响应函数,并在其中加上统计数据部分的代码:

-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
复制代码

这样作是能够的,可是现有工程中全部须要支持数据统计的按钮都必须替换成该子类,并且若是哪天不须要支持埋点功能了并须要迁移复用业务代码,那还得一个个再改回去。因此,咱们须要一个更优雅的实现。

咱们能够利用动态建立类并添加方法的思路来实现这个需求,这边只是以埋点做为示例,你也能够利用该思路扩展任意须要处理的需求和功能。简单来讲就是咱们建立一个UIButton的Category, 而后在须要埋点的状况下动态生成一个新的UIButton子类,并给其添加一个能够记录数据的事件响应方法来替代默认的方法,以下所示:

//
// 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); // 转换当前类从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
复制代码

而后在添加按钮的地方,若是须要数据统计功能,则调用enableEventTracking函数来内嵌打点功能。使用示例以下:

- (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:]
复制代码

浮于表面探究问题不失为一种方法,可是弄清楚本质才是真正意义上的解决疑惑。

参考文章

  1. 清晰理解Objective-C元类
  2. Objective-C Runtime 运行时之一: 类与对象
  3. What is a meta-class in Objective-C?
  4. 一只iOS魔法师的土系魔法讲义
相关文章
相关标签/搜索