Objective-C 的 runtime 特性与小蝌蚪找妈妈

Objective-C 是基于 C 语言加入了面向对象特性消息转发机制的动态语言。

面向对象消息转发是 Objective-C 两个最基本的核心所在。

runtime 运行时机制,就是用来进行动态创建类和对象,并进行消息发送的机制。

所以,可以毫不夸张的说,搞清楚了 runtime 机制,就是抓住了 Objective-C 语言的核心灵魂。

runtime 的消息转发机制,其实用一个字来概括,那就是——

这个过程,有点像小蝌蚪找妈妈,只有记住这一点,整个繁杂的消息转发机制很容易理解。

本文会在第三部分具体介绍 runtime 消息转发运行逻辑时详细解释。

小蝌蚪找妈妈,利用类比的方法,面向对象的去描述消息转发机制。

如果不想了解太多关于 runtime 的细碎知识,推荐直接跳到第三、四部分,这部分更加实用。

目录大概如下:

一、简要介绍

二、OC 与 C 语言的转化 — 相关术语

三、整个消息机制流程 — 小蝌蚪找妈妈

四、应用

一、简要介绍

1、 OC 是一门动态语言

OC 是一门动态语言,它可以把许多编译连接的工作推迟到运行时才做。

所以,它不仅仅需要编译器来编译代码,同时还需要一个运行时系统来执行编译之后的代码。

Objective-C Runtime 是一个 Runtime 库,主要是用 C 和汇编写的,这个库让基于 C 的 Objective-C 有了面向对象的能力。

运行时系统对于 Objective-C 来说就像是一个操作系统,有了它 Objective-C 才能正常运作。

2、 Objective-C 与 runtime 系统的交互

既然 runtime 系统类似于 OC 的底层操作系统,那么 OC 语言是怎么和 runtime 机制进行交互的呢?

OC 中一共有两种方法可以供我们使用:

(1)使用 NSObject 的方法。

NSObject 类作为 Cocoa 中最高层的元类,是大多数类的超级父类,所以很多类也继承了他的众多方法。

NSObject 里面有些方法就是用来获取 Runtime 系统信息的,这些方法可以让对象进行自我检查。形象的说这些方法,可以让一个类知道自己的爸爸是谁、自己到底天生有哪些方法、天生遵守了哪些协议

比如:

class 方法是返回某个对象所属的类;

isKindOfClass: 和 isMemberOfClass: 是检查对象是否在某个继承体系中;

respondsToSelector: 是检查对象是否能接受并响应某个信息;

conformToProtocol: 是检查对象是否遵守了某个协议;

methodForSelector: 是返回某个方法的具体实现的地址。

(2)使用 runtime 的函数

Runtime 是一个由一系列函数和数据结构组成的动态共享库( dynamic shared library ),并提供了一些公开的接口

正是因为这些公开接口的存在,可以让我们利用 runtime 特性做一些不太容易实现的特定需求。

所以我们也可以用纯 C 的代码来实现编译器编译 OC 代码后的效果。

如果想要使用这些 C 的代码,我们需要了解到底 runtime 把 OC 语言里面的类、方法、属性转换成了什么。

所以,下面具体解释一下相关的术语。

二、OC 与 C 语言的转化 — 相关术语

在 OC 中使用方法是这样的:

[self doSomething];

通过 runtime 会直接转换成消息发送函数:

obj_msgSend(self, @selector(doSomething));

它的声明是这样的:

// message.h
id obj_msgSend(id self, SEL op, …);

下面是对于以上代码的具体解释。

1、SEL

SEL 是转换之后的第二个参数的类型,对应于 OC 里面的 selector (方法选择器)。

SEL 的定义如下:

objc.h

typedef struct objc_selector *SEL;

其实它就是个映射到方法的 C 字符串,上面就是通过 @selector(doSomething) 来获取一个名字叫 doSomething 的 selector。

2、id

id 是转换之后的第一个参数的类型,对应于 OC 里面的 id 类型。在 OC 中 id 被称为是万能指针,可以指向任何对象。

它的定义如下:

// objc.h
typedef struct objc_object *id

struct objc_object {
Class isa;
};

3、Class

上面说的 isa 的类型是 Class,而它的定义如下:

 // objc.h
typedef struct objc_class *Class;

// runtime.h
struct objc_class {
    Class isa; // 类同样有自己父类,指向所属的父类

    Class super_class;  // 父类指针
    const char *name;   // 类名
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;  // 成员变量列表
    struct objc_method_list **methodLists; // 方法列表
    struct objc_cache *cache;  // 缓存
    struct objc_protocol_list *protocols; // 协议列表
};

可见在 Runtime 系统中,一个类还关联了它的父类指针、类名、成员变量、方法、缓存、协议。

注意到不仅表示对象的 objc_object 结构体中有个 isa 指针,表示类的 objc_class 结构体中也有个 isa 指针,这是因为在 OC 中,类本身也是一个对象(类对象)。

对象的方法存储在它所属的类中,那类的方法呢?

这时就需要类对象所属的类来存储类方法了,它叫 meta class(元类)。对象的类、父类、元类之间的关系如下(实现是 super_class 指针,虚线是 isa 指针):

注意到所有的元类的元类都是 root class(meta),而这个根元类的元类是它自己,它的父类是 NSObject;NSObject 的元类也是那个根元类,但它没有父类。

这样的类结构也充分体现了 OC 作为面向对象语言的一大特性————继承

每一个实例对象,一定可以通过自身的 isa 指针找到自己所属的类,每一个类对象(只要不是 NSObject 这样的 元类,都差不多是类对象),一定可以通过自身的 isa 指针找到自己所属的类。

这样清晰的继承关系,将会是 runtime 消息机制发送消息的基础。

后面降到 runtime 具体运行流程的时候,会详细解释。

附上继承的图例:

《Objective-C 的 runtime 特性与小蝌蚪找妈妈》 类的继承图.png

4、成员变量

其中 objc_ivar_list 是成员变量列表,定义如下:

    // runtime.h
struct objc_ivar_list {
    int ivar_count;
    int space;
    struct objc_ivar ivar_list[1];//成员变量的数组
}
struct objc_ivar {
    char *ivar_name; //单个变量的名字
    char *ivar_type; //变量类型
    int ivar_offset; //偏移量
    int space;
} 
typedef struct objc_ivar *Ivar;

可见成员变量列表 objc_ivar_list 结构体存储着由成员变量 objc_ivar 结构体组成的数组,objc_ivar 结构体存储着单个成员变量的名字、类型、偏移量等信息。

5、方法

objc_method_list 是方法列表,定义如下:

    // runtime.h
    struct objc_method_list {
    struct objc_method_list *obsolete;
    int method_count;
    int space;
    struct objc_method method_list[1];// 方法数组
    }
    
    struct objc_method {
    SEL method_name; // 方法名
    char *method_types; // 方法类型
    IMP method_imp; // 方法实现
    }
    
    typdef struct objc_method *Method;

方法列表的组成逻辑基本和成员变量列表的构成逻辑一样的。

方法列表 objc_method_list 结构体存储着由方法 objc_method 结构体组成的数组,objc_method 结构体存储着单个方法的信息:名称(SEL类型的)、参数类型和返回值类型(method_types中)和具体实现(IMP类型的)。

6、IMP(这是一个 OC 里面没有的对象概念)

IMP(method implementation,方法实现) 的定义是:

// objc.h
typedef id (*IMP)(id, SEL, ...);

所以它其实是一个函数指针,指向某个方法的具体实现。它的类型和 objc_msgSend 函数相同,参数中也都包含有 id 和 SEL 类型,这是因为一个 id 和 一个 SEL 参数就能确定唯一的方法实现地址。

7、Cache(被调用过的函数的缓存,OC里也没有这个概念)

在 objc_class 结构体中还有个指向 objc_cache 结构体的指针,它的定义如下:

// runtime.h
typedef struct objc_cache *Cache
// objc-cache.m
struct objc_cache {
    // 当前能达到的最大 index
    uintptr_t mask;          
    // 被占用的槽位。因为缓存是以散列表的形式存在,所以会有空槽
    uintptr_t occupied;        
    // 用数组表示的 hash 表
    cache_entry *buckets[1];
};
typedef struct {
    SEL name;    
    void *unused;
    IMP imp;  
} cache_entry;
// _uintptr_t.h
typedef unsigned long uintptr_t;

所以它用来做缓存的,用 buckets 数组来存储被调用过的方法

因为一个方法被调用过,那它以后有可能还会被调用,所以将其存储起来,下次要找某方法先到缓存中找,如果找到的话,免去后面的寻找过程,速度虽然仍会比直接调用函数慢一点点,但已经有很大提升。

8、属性
还有我们常用的属性其实也是结构体,它的定义如下:

// runtime.h
typedef struct objc_property *objc_property_t;
typedef struct {
    const char *name;
    const char *value;
} objc_property_attribute_t;

// objc-runtime-new.h
typedef struct objc_property {
    const char *name;   // 属性名称
    const char *attributes; // 属性字符串
} property_t;

typedef struct property_list_t {
    uint32_t entsize;
    uint32_t count;
    property_t first;
} property_list_t;

所以一个 property_t 结构包含了属性的名称和属性字符串。与属性相关的一些方法如下:

    #define newproperty(p) ((property_t *)p)
// 返回协议中的属性列表,属性个数存储在参数 outCount 中
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
// 返回类中的属性列表,属性个数存储在参数 outCount 中
objc_property_t *class_copyPropertyList(Class cls_gen, unsigned int *outCount)
// 返回属性列表中的属性数组,属性个数存储在参数 outCount 中
static property_t **copyPropertyList(property_list_t *plist, unsigned int *outCount)
// 返回类中的特定名字的属性
objc_property_t class_getProperty(Class cls_gen, const char *name)
// 返回某个属性的名字
const char *property_getName(objc_property_t prop)
// 返回某个属性的属性字符串
const char *property_getAttributes(objc_property_t prop)

上面一一介绍了 OC 里面的常见类型转换成 C 的对应代码。

我们看到,OC 的类和对象由于使用了继承的机制,都是有自己的 isa 指针。都是在 C 语言的结构体里面的。

对象的成员变量、方法名、方法实现、方法缓存、属性,几乎在 C 语言里都是结构体数组的组合。

通过以上的一一对应的描述,基本了解了 runtime 把 OC 代码转成了相应的 C 语言代码的具体样式。

下面,可以具体来看一看 runtime 到底是怎样运行的了。

三、整个消息机制流程 —— objc_msgSend

上面的介绍,有一个地方让人不太好理解。

为什么有方法名,还需要一个 IMP 类型的方法实现呢?

这种 IMP 类型的存在,其实就是由 runtime 使用对象方法时候的消息机制决定的。

使用某个对象的方法,其实在 runtime 里面都是给这个对象发送消息

消息和方法实现直到运行时才会绑定。

整个消息发送机制最重要的部分就是:消息(里面包含方法名)与方法实现绑定之前,消息方法实现的过程。

Runtime 系统会把使用方法转换为调用函数:

objc_msgSend(receiver, selector)

注意,此时函数多了两个参数:消息接受者方法的selector

这是每个方法调用时都会默认存在的隐藏参数。如果还有其他参数则是:

objc_msgSend(receiver, selector, arg1, arg2, ...)

objc_msgSend 要做的事件有三件:

(1)找到 selector 对应的方法实现;
(2)调用该方法实现,并把消息接收者(如果有参数则加上那些参数)传给它;
(3)把方法实现的返回值传回去(它自己并没有任何返回值)。

其中第(1)件事的最关键的。

runtime 一旦发送了调用方法的消息,那么,这个消息就会带着方法名,进入漫长的过程去找到自己的方法实现

可以用一个字来形容 runtime 的消息机制的核心,那就是——

发出的消息,寻找自己方法实现的过程。可以类比成我们小时候学过的课文:小蝌蚪找妈妈

为了便于理解。这里首先明确一下,类比的关系。

在上面这一段代码里面:receiver 是一个类,是整个消息的接受者。这个receive 类里面,可能有 selector 对应的方法实现,也可能没有。

receiver类,可以想象成是小蝌蚪妈妈的家,方法实现才是小蝌蚪的妈妈。

简单对应关系是:

receiver 类 ——蝌蚪妈妈存在的地方
selector ——- 带着方法名的小蝌蚪
receiver 类里面的 方法实现 ——- 小蝌蚪的妈妈

记住上面的三个对应关系,下面的流程就会很简单。如果,你在后面的过程中不太理解,请翻到这里在看一遍。

这个小蝌蚪找妈妈的过程又分为两种类型:

1、小蝌蚪知道自己妈妈的名字(也就是方法名),也确实能够找到自己的依然健在的妈妈(方法实现是有的)

2、小蝌蚪知道自己妈妈的名字(也就是方法名),但是自己妈妈(方法实现)确实就是不存在的。(你可以理解为没有写方法实现,小蝌蚪的妈妈去世了)

如果是第一种类型,我们的小蝌蚪找妈妈的过程将会是很顺利的。

但是,如果是第二种情况,方法名存在,但是在对象和对象的继承体系里根本找不到方法实现

那么,就会出现在编程中最常见的错误——unrecognized selector sent to…。这相当于,小蝌蚪费劲千辛万苦,最后发现自己的妈妈已经不在人间了。唉,可怜的小蝌蚪。

都是这个就在这个时候,神奇的 iOS 程序员就该上场了。

因为,runtime 机制在宣告unrecognized selector sent to…(小蝌蚪的妈妈不在人世)之前,程序员是有三次机会去实现方法的。是可以给可怜的小蝌蚪人工培育出一个后妈的,尽管不是亲生的妈妈。

好,讲了这么多,其实只是想让你记住,runtime 发消息的核心就是——

下面我们开始小蝌蚪找妈妈的过程吧。如果实在找不到,我们就给小蝌蚪人工培育一个妈妈。

起点是这一句代码:

objc_msgSend(receiver, selector, arg1, arg2, …)

这个过程一共有 8 步,下面以序号一次讲解。

(1)检查该 selector 是不是要忽略的;(小蝌蚪这条消息,本身是不是就没有呢。)

这个过程,我个人理解是,该 selector 是不是根本就没有在 .h 文件中声明。也就是,小蝌蚪可能现在就不存在。

(2)检查这个 target 是否为 nil。在 OC 中给 nil 发送任何消息都不会出错,返回的结果都是 0 或 nil。

这一步可以理解为:receiver 类或对象是否为 nil。小蝌蚪需要去一个地方(类或对象)找妈妈(receiver 类里面的方法实现),如果那个地方根本不存在,那妈妈肯定也不会存在的。

(3)开始在查找这个类(小蝌蚪妈妈的家)里面 IMP (方法实现)。先在 cache (小蝌蚪妈妈的员工宿舍)中找,找到则调到对应的方法实现中去执行。

(4)在 cache 中没找到,则在该类的方法分发表(dispatch table,即方法列表)中找,找到则执行。

(5)在该类的方法分发表中找不到,则到父类(其实就是小蝌蚪的外婆家)的分发表中找,再找不到则往上找,直到 NSObject 类(这估计就是小蝌蚪家族的最大年级的祖宗家里)为止。这两个过程的示意图如下:

《Objective-C 的 runtime 特性与小蝌蚪找妈妈》 找实现示意图.png

前 5 步,都找完了。如果还是没有找到方法实现(小蝌蚪的妈妈)。runtime 系统此时,还不敢确定到底这个方法实现(小蝌蚪的妈妈)是否存在。

在报错之前,也就是这就是这样的报错:unrecognized selector sent to…(小蝌蚪的妈妈不在人世)

上面说过有三次机会。

第一次机会是我们下面的第6步。

(6)这一步叫做:动态方法解析(Dynamic Method Resolution)。

这是 Runtime 系统在报错前给我们的第一次补救的机会,它会调用 resolveInstanceMethod: 或者 resolveClassMethod: 方法,所以我们可以在这两方法中分别用 class_addMethod 给某个类或对象的某个 selector 动态添加一个方法实现(这相当于人工培育一只青蛙给小蝌蚪做妈妈)。

如在 main 函数中调用 Receiver 对象的一个 kedouMotherResolveMethod 方法:

Receiver *receiver = [[Receiver alloc] init];
[receiver kedouMotherResolveMethod];

它的 .h 和 .m 文件如下:
// Receiver.h
#import <Foundation/Foundation.h>

@interface Receiver : NSObject

///蝌蚪妈妈的名字 动态解析方法
- (void)kedouMotherResolveMethod;

///蝌蚪妈妈的名字 重定向方法
- (void)kedouMotherRedirectMethod;

///蝌蚪妈妈的名字 消息转发方法
- (void)kedouMotherforwardMethod;

@end


//  Receiver.m
#import "Receiver.h"
#import <objc/runtime.h>

    // 要被动态添加的方法
void kedouMotherIMP(id self, SEL _cmd){
    NSLog(@"我是程序员使用动态方法解析,人工培育的蝌蚪妈妈");
}

@implementation Receiver

// 补救第一步:动态解析方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod");

    //如果这个 sel  是需要动态添加的 (也就是小蝌蚪的妈妈是我们人工培育的)
    if (sel == @selector(kedouMotherResolveMethod)) {//这里只是判断方法名,也就是小蝌蚪消息里面的方法名
        //把实现方法和消息的里面的方法
        class_addMethod([self class], sel, (IMP)kedouMotherIMP , "@");
        // 返回 YES 后, Runtime 重新给对象发送 kedouMother 消息,这次就可以找到 kedouMotherIMP 方法实现并调用它了
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

@end

(7)重定向:如果在上面的方法中不做处理或返回 NO,Runtime 系统在报错前还会给第二次补救机会,就是会调用 forwardingTargetForSelector: 方法索要一个能响应这个消息的对象,所以我们可以在这里返回另外一个能处理该消息的对象:

    //Receiver.m
    // 补救第二步:重定向

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardInvocation");
    
    if (aSelector == @selector(kedouMotherRedirectMethod)) {//这里只是判断方法名,也就是小蝌蚪消息里面的方法名
        
        // 返回另外一个对象,让它去接收该消息
        return [[StepMother alloc] init];
    }
    return  [super forwardingTargetForSelector:aSelector];
}

上面返回的是一个 StepMother 对象,如果 StepMother 类定义如下:

// StepMother.h
#import <Foundation/Foundation.h>

@interface StepMother : NSObject

- (void)kedouMotherRedirectMethod;///蝌蚪妈妈的名字 重定向方法

- (void)kedouMotherforwardMethod;///蝌蚪妈妈的名字 消息转发方法

@end

//  StepMother.m
#import "StepMother.h"
@implementation StepMother
- (void)kedouMotherRedirectMethod {
NSLog(@"StepMother kedouMotherRedirectMethod");
NSLog(@"我是程序员使用重定向方法,人工培育的蝌蚪妈妈");
}
@end

则输出结果就是 “StepMother kedouMotherRedirectMethod”了。

(8)消息转发:如果在上一步中不做处理或者返回 nil 或 self,则 Runtime 系统会在报错前给我们最后一次补救机会。为了可以不让小蝌蚪成为孤儿,runtime 机制,可真是操碎了心。

好吧,那就看看到底是怎么玩的。

系统会先调用 methodSignatureForSelector: 方法,在该方法返回一个包含了消息的描述信息的方法签名(NSMethodSignature对象),并用此方法签名去生成一个 NSInvocation 对象,然后调用 forwardInvocation: 方法并把刚生成的 NSInvocation 对象作参数传进去。

我们可以重写 forwardInvocation: 方法,在这里将消息转发给其他对象(人工培育的青蛙妈妈):

    //获取一个方法签名,用于生成 NSInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"methodSignatureForSelector");
    
    if ([NSStringFromSelector(aSelector) isEqualToString:@"kedouMotherforwardMethod"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//补救第三步:消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
    //如果另外一个对象 stepMother 可以响应该方法
    if ([[[StepMother alloc] init] respondsToSelector:[anInvocation selector]]) {
        // 则让另一个对象来响应该方法
        [anInvocation invokeWithTarget:[[StepMother alloc] init]];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

尽管消息转发的效果类似于多继承,让一个对象看起来能处理自己不拥有的方法,但 NSObject 类不会将两者混淆。如上面的例子, [p respondsToSelector:@selector(aMethod)] 的结果还是 NO。

点击这里获取以上案例的Demo

在这三次的补救中,我们可以添加一个方法的方法实现。同时,我们挽救了一只将要成为孤儿的小蝌蚪,可以说是功德无量。

那下面在具体看一看,在项目中,我们到底有哪些应用。

四、应用

以下代码的Demo,我就没有自己去写了。

可以参见大神的Demo

1、实现自定义的 tabBar

大多 App 都是使用继承自 UITabBarController 的自定义控制器做 window 的 rootViewController,系统提供的 tabBar 可能满足不了我们的需求,此时我们可以通过以下方法使用我们自定义的 tabBar 并布局其中的按钮:

    //  YGMainViewController.m
    - (void)viewDidLoad {
        [super viewDidLoad];
        //创建并使用自定义的 tabBar
        YGMainTabBar *mainTarBar = [YGMainTabBar new];
        [self setValue:mainTarBar forKey:@"tabBar"];
    }
    //  YGMainTabBar.m
    - (void)layoutSubviews {
        [super layoutSubviews];
        for (UIView *subView in self.subviews) {
            if ([subView isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
                // 布局按钮
            }
        }
    }

2、获取属性名 — MJExtension 有经典应用

我们在用字典生成模型时一般是使用 -setValuesForKeysWithDictionary: 方法来赋值,并用 – setValue:forUndefinedKey: 方法来过滤掉多余的键值。我们也可以用 Runtime 提供的方法来获取某个类的共有属性名,再逐一使用 – setValue:forKey: 进行 KVC 赋值:

// 类方法:字典 --> 模型, KVC
+ (instancetype)cycleWithDict:(NSDictionary *)dict{
    id obj = [[self alloc] init];
    
    for (NSString *key in [self publicProperties]) {
        if (dict[key]) {
            [obj setValue:dict[key] forKey:key];
        }
    }
    
    return obj;
}


// 通过 runtime 方法获取所有公有属性名
+ (NSArray *)publicProperties{
    unsigned int count = 0;
    // 获取当前类的属性列表(即数组)
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    
    NSMutableArray *ocProperties = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        // 取出每一个属性
        objc_property_t property = propertyList[i];
        // 取出属性名
        const char *cPropertyName = property_getName(property);
        // C --> OC
        NSString *ocPropertyName = [[NSString alloc] initWithCString:cPropertyName
                                                            encoding:NSUTF8StringEncoding];
        
        [ocProperties addObject:ocPropertyName];
    }
    
    // 释放
    free(propertyList);
    
    return ocProperties.copy;
}

3、关联属性 — (这个应该是用的相对较多的,比较常见)

提示:MJRefresh 里面的UIScrollerView 本身就是利用关联属性在category 里面添加 header 和 footer的。

我们还可能希望给某些常用的类添加 category,但 category 是只能添加方法而不能添加存储属性的。现在我们可以用 Runtime 来间接在 category 添加属性了,如在给 UIButton 的 category 中添加一个属性作回调:

//  UIButton+Extension.h
#import <UIKit/UIKit.h>
typedef void (^CallbackBlock)();
@interface UIButton (Extension)
@property (copy, nonatomic) CallbackBlock callback;
@end

//  UIButton+Extension.m
#import "UIButton+Extension.h"
#import <objc/runtime.h>
const void *yg_callbackKey = @"yg_callbackKey";
@implementation UIButton (Extension)

- (void)setCallback:(CallbackBlock)callback {
    // 设置关联属性
    objc_setAssociatedObject(self, yg_callbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (CallbackBlock)callback {
    // 获取关联属性
    return objc_getAssociatedObject(self, yg_callbackKey);
}

@end
这样就可以把 callback 当做按钮的属性来用了:

//  ViewController.m
#import "ViewController.h"
#import "UIButton+Extension.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *button;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置按钮的 callback “属性”的内容
    self.button.callback = ^{
        NSLog(@"button callback");
    };
    // 获取并执行按钮的 callback “属性”
    self.button.callback();
}

@end
我们常用的第三方库中有很多也是这样用的,如 SDWebImage 会用这样的方法来存储传进来的图片的 URL:

// UIImageView+WebCache.m
- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_cancelCurrentImageLoad];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    ...
}
- (NSURL *)sd_imageURL {
    return objc_getAssociatedObject(self, &imageURLKey);
}

4、Method Swizzling (俗称:黑魔法)

具体看这篇文章。OC中Method Swizzling的原理及应用

本文参考:

1、Objective-C Runtime

2、iOS – NSInvocation的使用

更多链接:
面试题1
笔试题2

    原文作者:吴佩在天涯
    原文地址: https://www.jianshu.com/p/19bf451dc157
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞