OC-RunTime: 总结消息转发中用到的知识点

前言

OC-RunTime: 消息转发之实例方法的转发流程

OC-RunTime: 消息转发之实例方法的转发流程实例讲解

OC-RunTime: 消息转发之类方法的转发流程

在上面的几篇文章中我分享了关于消息转发相关的知识点,里面有很多细节没有阐述。

若在上面的文章中加入很多细节点的话,一是拉长了文章的内容, 二是对于刚接触 Runtime 的朋友来说不一定能接受, 于是就有了这篇文章的诞生.

RunTime 的定义及使用场景

苹果 开发文档 的这样解释 runtime 的:

1
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work

尽量将决定放到运行的时候,而不是在编译和链接过程中.

RunTime 的应用场景:

1.面向切面编程 AOP.
2.方法调配 method swizzling. 有些地方称之为”黑魔法”.
3.消息转发.
4.给分类添加属性(关联对象).
5.动态获取 class 和 slector.
6.KVO/KVC, 修改私有属性的值.

建议去阅读下面框架的源码,恕我没有加入链接:

Aspects(AOP必备,“取缔” baseVC,无侵入埋点)

MJExtension(JSON 转 model,一行代码实现 NSCoding 协议的自动归档和解档)

JSPatch(动态下发 JS 进行热修复)

NullSafe(防止因发 unrecognised messages 给 NSNull 导致的崩溃)

UITableView-FDTemplateLayoutCell(自动计算并缓存 table view 的 cell 高度)

UINavigationController+FDFullscreenPopGesture(全屏滑动返回)

思考问题

在前面的文章中,很多次看到 IMP、SEL、selector 以及 Method 等关键字,相信大家随着对 RunTime 的逐步了解,慢慢会逐渐熟悉它们的,只是时间问题。很多概念上面的东西理解起来没那么简单,需要动手去写写代码。

在看下面内容之前, 先抛出一个问题:

runtime 如何通过 selector 找到对应的 IMP 地址?

接下来分别说一下 IMP、SEL、selector 以及 Method.

IMP

IMP 保存的是 Method 的地址,本质是一个函数指针,由编译器生成。

IMP 在 objc.h 中的定义:

1
2
3
4
5
6
/// 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

向对象发送消息之后,是由这个函数指针 IMP 指定的, 即 IMP 函数指针就指向了方法的实现.

IMP 函数指针最少包含 id 和 SEL 类型的两个参数,后面其他的参数是对应方法需要的参数。其中 id 代表执行该方法的 target(对象), SEL 就是对应的方法, 通过 id 和 SEL 参数就能确定唯一的方法实现地址.

那么我们如何获取方法的 IMP 呢?很简单.

NSObject 提供了如下两个方法:

1
2
- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;

对应的实现(源码 NSObject.mm), 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (IMP)instanceMethodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return class_getMethodImplementation(self, sel);
}
+ (IMP)methodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return object_getMethodImplementation((id)self, sel);
}
- (IMP)methodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return object_getMethodImplementation(self, sel);
}

大家可以看到,对应的 methodForSelector 既有实例方法又有类方法,而 instanceMethodForSelector 只有类方法。

在使用 methodForSelector 方法时,向类发送消息,则 SEL 应该是类方法, 若向实例对象发送消息,则 SEL 应该为实例对象方法.

instanceMethodForSelector 仅仅允许类发送该消息, 从而获取实例方法的 IMP. 该方法无法获取类方法的 IMP, 如果想获取类方法的 IMP 可以使用 methodForSelector 来获取。

函数文档原文解释如下:

1
2
Use this method to ask the class object for the implementation of instance methods only.
To ask the class for the implementation of a class method, send the methodForSelector: instance method to the class instead.

举个例子,或许更好理解。

下面两个方法, 一个是类方法(testClassMethod), 另一个是实例方法(testInstanceMethod).

1
2
3
4
5
+ (void)testClassMethod {
}
- (void)testInstanceMethod {
}

分别使用上面提到的方法来获取 IMP 的几个方法.

1
2
3
4
5
6
7
8
9
10
IMP imp = [[self class] instanceMethodForSelector:@selector(testClassMethod)];
IMP imp2 = [[self class] instanceMethodForSelector:@selector(testInstanceMethod)];
// 也可以改成 NSObject 调用的方式, 结果一样.
// IMP imp = [NSObject instanceMethodForSelector:@selector(testClassMethod)];
// IMP imp2 = [NSObject instanceMethodForSelector:@selector(testInstanceMethod)];
IMP imp3 = [[self class] methodForSelector:@selector(testClassMethod)];
IMP imp4 = [self methodForSelector:@selector(testInstanceMethod)];

调试器可以看出, 如下日志:

1
2
3
4
5
6
7
8
9
10
11
12
Printing description of imp:
(IMP) imp = 0x000000010d8455c0 (libobjc.A.dylib`_objc_msgForward)
Printing description of imp2:
(IMP) imp2 = 0x000000010cf19b90 (-[ViewController testInstanceMethod] at ViewController.m:94)
Printing description of imp3:
(IMP) imp3 = 0x000000010cf19b60 (+[ViewController testClassMethod] at ViewController.m:89)
Printing description of imp4:
(IMP) imp4 = 0x000000010cf19b90 (-[ViewController testInstanceMethod] at ViewController.m:94)
(lldb)

imp2、imp3、imp4 都是正常的,唯独 imp 不正常,也充分说明了 instanceMethodForSelector 无法获取类方法的 IMP.

Method

在源码 runtime.h 中, 定义 method, 其本质是一个结构体.

1
2
3
4
5
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

方法名 method_name 类型为 SEL.

method_types 方法类型, 是一个 char 指针,存储着方法的参数类型和返回值类型。

方法实现 method_imp 的类型为 IMP.

可以看出, 有 SEL 和 IMP, method_types 是对应的方法返回值和参数类型, 如 v@:,是一个字符串。

runtime.h 中有两个方法,可以根据 SEL 直接获取实例方法和类方法的 Method,如下:

1
2
3
Method class_getInstanceMethod(Class cls, SEL name);
Method class_getClassMethod(Class cls, SEL name);

SEL

selector, 称之为方法选择器,SEL 是 selector 的表示类型,也是方法的编号,是类成员方法的指针。

SEL 定义在源码 objc.h 中, 是一个结构体指针, 如下:

1
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

但是源码中查不到 objc_selector 具体的定义和实现.

获取 SEL 有三个方法:

1
2
3
SEL sel = @selector(play:);
SEL sel = sel_registerName("play:");
SEL sel = NSSelectorFromString(@"play");

SEL 表示一个 selector 的指针,无论什么类里,只要方法名相同,SEL 就相同,SEL 实际是根据方法名 hash 化了的字符串。而对于字符串的比较仅仅需要比较他们的地址就可以了,所以速度上非常快,SEL 的存在加快了查询方法的速度。

思考一个问题:为什么在同一个 OC 类中,不能存在同名的函数,即使参数类型不同也不行,换句话说 OC为什么没有重载?

答案已经在上面说了,SEL 表示一个 selector 的指针,无论什么类里,只要方法名相同,SEL 就相同,相同的函数名,编译器无法编译通过。

dispatch table 存放 SEL 和 IMP 的对应关系,SEL 最终会通过 dispatch table 寻找到对应的IMP。

总之,Selector、Method 和 IMP 三者之间的关系可以这么解释,在类的(实例和类方法)调度表(dispatch table)中的每一个实体代表一个方法 Method,其名字叫做选择器 SEL,并对应着一种方法实现称之为 IMP,有了 Method 就可以使用 SEL 找到对应的 IMP,SEL 就是为了查找方法的最终实现 IMP。

class_addMethod

查看源码 objc-runtime-new.mm 中该函数实现如下:

1
2
3
4
5
6
7
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
rwlock_writer_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}

开发文档中这样描述该函数:

1
2
3
4
Adds a new method to a class with a given name and implementation.
class_addMethod will add an override of a superclass's implementation,
but will not replace an existing implementation in this class.
To change an existing implementation, use method_setImplementation.

解释一下,可以为类根据 SEL 和 IMP 动态添加一个新方法。class_addMethod 仅可以动态添加方法,不会替换。如果想达到方法替换的效果可使用 method_setImplementation 函数。

关于 method_setImplementationmethod_exchangeImplementations 后面文章再做分析.

其实, method_exchangeImplementations 的内部实现相当于调用了 2 次 method_setImplementation 方法。

class_addMethod 不仅可以动态添加类方法, 也可以添加实例方法。

参数及返回值解释:

1
2
3
4
5
6
7
8
9
返回值: 返回 YES 表示方法添加成功, 否则添加失败。
参数 Class cls: 将要给添加方法的类, 即[类名 class]
参数 SEL name: 将要添加的方法 SEL, 即 @selector(方法名),如果已经存在,该方法返回失败,不存在就添加成功。
参数 IMP imp:实现这个方法的函数. 有两种写法即 C 和 OC 的写法. 一个 IMP 最少包括两个参数, 上面已经说过。
参数 const char *types: 实现方法的函数的返回和参数编码类型. 如 "v@:" 表示返回值为 void, 没有参数的一个函数, 其中 @和:分别代表 IMP 的默认两个参数即 id 和 sel.

关于 types,可以使用 method_getTypeEncoding 来获取。

更多关于 types 的内容可以参考开发者文档 Type Encodings.

解答问题

读到这里, 大家对 IMP, SEL 以及 Method 应该有初步的了解了, 那么来解答一下刚才提出的问题:

runtime 如何通过 selector 找到对应的 IMP 地址?

回答这个问题的关键是要知道消息调度表(dispatch table),另外一个要回答的要点是 IMP 的实现和获取以及和 Method 之间的关系。

类对象中有类方法和实例方法的分发表,表中记录着方法的名字、参数和实现,selector 本质就是方法名称,runtime 通过这个方法名称就可以在列表中找到该方法对应的实现.

系统为我们提供了获取 IMP 指针的函数,无论是类方法还是实例方法我们都可以获取对应的 IMP.

而 Method 将 Selector 和 IMP 联系起来,可从源码中看出:

1
2
3
4
5
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

IMP 是函数的指针,它是由编译器编译生成的。当发一个消息时,它会找到那段代码执行,IMP 指向了这个方法的具体的实现,得到这个函数的指针可以直接执行。

IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址,反之亦然。当发送消息给一个对象时,runTime 会在对象的类对象方法列表里查找,当我们发送一个消息给一个类时,这条消息会在类的 Meta Class 对象的方法列表里查找,直到超找到 NSObject 中为止。

消息传递的过程

1. 当消息被发送给一个对象,messaging function 跟随对象的 isa 指针找到它的 class structure,在 dispatch table 中寻找 method selector.

2. 如果没有找到 selector,objc_msgsend 跟随该类实例的 isa 找到父类,尝试在父类的 dispatch table 中寻找 selector.

3. 重复步骤 2,直到 isa 指向 NSObject Class 为止。

关于分发表和消息相关的知识可以参考开发文档 Messaging,讲得很清楚。

实际例子

说了这么多理论知识,是时候举栗子了,方便大家更好的理解上面的内容。

1. 动态添加实例方法

Student.m

init 外,Student 只有一个实例方法 studentWalkImp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@implementation Student
- (instancetype)init
{
self = [super init];
if (self) {
SEL proxySelector = NSSelectorFromString(@"studentWalkImp");
IMP impletor = class_getMethodImplementation([self class], proxySelector);
// 获取实例方法
Method method = class_getInstanceMethod([self class], proxySelector);
const char *types = method_getTypeEncoding(method);
SEL origSel = NSSelectorFromString(@"walk");
class_addMethod([self class], origSel, impletor, types);
}
return self;
}
- (void)studentWalkImp
{
NSLog(@"---veryitman--- Student studentWalkImp");
}
@end

调用测试一下.

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];
Student *stud = [[Student alloc] init];
[stud performSelector:NSSelectorFromString(@"walk") withObject:nil];
}

这里 Student 并没有 walk 方法,故意为之,运行后控制台会打印:

1
---veryitman--- Student studentWalkImp

成功的为 Student 添加了一个实例方法 walk 的实现 studentWalkImp.

上面的例子是使用 OC 的 IMP 方式来实现的,可以改为 C 实现版本的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation Student
- (instancetype)init
{
self = [super init];
if (self) {
SEL origSel = NSSelectorFromString(@"walk");
class_addMethod([self class], origSel, (IMP)studentWalkImp, "v:@");
}
return self;
}
void studentWalkImp()
{
NSLog(@"---veryitman--- Student studentWalkImp");
}

2. 动态添加类方法

动态添加类方法,和动态添加实例方法稍微有点不同。下面是改造后的 Student.m.

Student.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@implementation Student
- (instancetype)init
{
self = [super init];
if (self) {
// 获取 MetaClass, 类方法不可以使用 [self class]
Class metaCls = objc_getMetaClass([NSStringFromClass([self class]) UTF8String]);
SEL proxySelector = NSSelectorFromString(@"clsImp");
IMP impletor = class_getMethodImplementation(metaCls, proxySelector);
// 获取类方法
Method method = class_getClassMethod([self class], proxySelector);
const char *types = method_getTypeEncoding(method);
SEL origSel = NSSelectorFromString(@"walk");
class_addMethod(metaCls, origSel, impletor, types);
}
return self;
}
+ (void)clsImp
{
NSLog(@"---veryitman--- Student clsImp");
}

注意:这里获取 Class 稍微不同的是使用了 objc_getMetaClass,这里关系到 Objective-C 中的类、Class、根类和元类的区别,后续博文再做分享。

调用测试一下:

1
2
3
4
5
6
7
8
- (void)viewDidLoad {
[super viewDidLoad];
Student *stud = [[Student alloc] init];
[[stud class] performSelector:NSSelectorFromString(@"walk") withObject:nil];
}

控制台打印:

1
---veryitman--- Student clsImp

成功地为类动态的添加了一个类方法 clsImp.

参考文档

1. Objective-C 对象模型及应用

2. Apple RunTime 源码 objc4-723.tar.gz

3. Messaging

4. Objective-C 深入理解中的消息机制和方法调用

完整代码

点击下载文中完整的 Demo.


扫码关注,你我就各多一个朋友~

坚持原创技术分享!