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 以及 Method 等关键字, 随着大家后面对 RunTime 的了解会逐渐跟他们越发熟悉.

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

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

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

IMP

大家对 Objective-C 里面的 IMP 并不陌生, IMP 是 Objective-C 方法(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;
}

可以看出, 有 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

iOS 开发中, 经常看到和使用 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");

从上面可以看出 IMP/Method/Selector 三者之间是密切关联的, SEL 就是其中的桥梁.

总之, 在类的方法(实例和类方法)调度表(dispatch table, 也可以说是分发表)中的每一个实体代表一个方法 Method, 其名字叫做选择器 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.

解释一下:

1
2
3
可以为类根据 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 地址
?

回答这个问题的关键是要知道消息调度表(也叫分度表).
正是因为有了这个表, runtime 才能游刃有余. 另外一个要回答的要点是 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 对象的方法列表里查找.

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

例子

上面说了很多理论知识, 下面举个例子, 更好的理解一下上面的内容.

动态添加实例方法

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
19
@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");
}

动态添加类方法

动态添加类方法, 和动态添加实例方法稍微有点不同. 下面是改造后的 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

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

参考

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

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

3.Messaging

完整代码

点击下载文中完整的 Demo.

坚持原创技术分享!