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 | /// A pointer to the function of a method implementation. |
向对象发送消息之后,是由这个函数指针 IMP 指定的, 即 IMP 函数指针就指向了方法的实现.
IMP 函数指针最少包含 id 和 SEL 类型的两个参数,后面其他的参数是对应方法需要的参数。其中 id 代表执行该方法的 target(对象), SEL 就是对应的方法, 通过 id 和 SEL 参数就能确定唯一的方法实现地址.
那么我们如何获取方法的 IMP 呢?很简单.
NSObject 提供了如下两个方法:
1 | - (IMP)methodForSelector:(SEL)aSelector; |
对应的实现(源码 NSObject.mm), 如下:
1 | + (IMP)instanceMethodForSelector:(SEL)sel { |
大家可以看到,对应的 methodForSelector
既有实例方法又有类方法,而 instanceMethodForSelector
只有类方法。
在使用 methodForSelector
方法时,向类发送消息,则 SEL 应该是类方法, 若向实例对象发送消息,则 SEL 应该为实例对象方法.
而 instanceMethodForSelector
仅仅允许类发送该消息, 从而获取实例方法的 IMP. 该方法无法获取类方法的 IMP, 如果想获取类方法的 IMP 可以使用 methodForSelector
来获取。
函数文档原文解释如下:
1 | Use this method to ask the class object for the implementation of instance methods only. |
举个例子,或许更好理解。
下面两个方法, 一个是类方法(testClassMethod), 另一个是实例方法(testInstanceMethod).
1 | + (void)testClassMethod { |
分别使用上面提到的方法来获取 IMP 的几个方法.
1 | IMP imp = [[self class] instanceMethodForSelector:@selector(testClassMethod)]; |
调试器可以看出, 如下日志:
1 | Printing description of imp: |
imp2、imp3、imp4 都是正常的,唯独 imp 不正常,也充分说明了 instanceMethodForSelector
无法获取类方法的 IMP.
Method
在源码 runtime.h
中, 定义 method, 其本质是一个结构体.
1 | struct objc_method { |
方法名 method_name
类型为 SEL.
method_types
方法类型, 是一个 char 指针,存储着方法的参数类型和返回值类型。
方法实现 method_imp
的类型为 IMP.
可以看出, 有 SEL 和 IMP, method_types 是对应的方法返回值和参数类型, 如 v@:
,是一个字符串。
runtime.h
中有两个方法,可以根据 SEL 直接获取实例方法和类方法的 Method,如下:
1 | Method class_getInstanceMethod(Class cls, SEL name); |
SEL
selector
, 称之为方法选择器,SEL 是 selector
的表示类型,也是方法的编号,是类成员方法的指针。
SEL 定义在源码 objc.h
中, 是一个结构体指针, 如下:
1 | /// An opaque type that represents a method selector. |
但是源码中查不到 objc_selector
具体的定义和实现.
获取 SEL 有三个方法:
1 | SEL sel = @selector(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 | BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) |
开发文档中这样描述该函数:
1 | Adds a new method to a class with a given name and implementation. |
解释一下,可以为类根据 SEL 和 IMP 动态添加一个新方法。class_addMethod
仅可以动态添加方法,不会替换。如果想达到方法替换的效果可使用 method_setImplementation
函数。
关于 method_setImplementation
和 method_exchangeImplementations
后面文章再做分析.
其实, method_exchangeImplementations
的内部实现相当于调用了 2 次 method_setImplementation
方法。
class_addMethod
不仅可以动态添加类方法, 也可以添加实例方法。
参数及返回值解释:
1 | 返回值: 返回 YES 表示方法添加成功, 否则添加失败。 |
关于 types,可以使用 method_getTypeEncoding
来获取。
更多关于 types 的内容可以参考开发者文档 Type Encodings.
解答问题
读到这里, 大家对 IMP, SEL 以及 Method 应该有初步的了解了, 那么来解答一下刚才提出的问题:
runtime 如何通过 selector 找到对应的 IMP 地址?
回答这个问题的关键是要知道消息调度表(dispatch table),另外一个要回答的要点是 IMP 的实现和获取以及和 Method 之间的关系。
类对象中有类方法和实例方法的分发表,表中记录着方法的名字、参数和实现,selector 本质就是方法名称,runtime 通过这个方法名称就可以在列表中找到该方法对应的实现.
系统为我们提供了获取 IMP 指针的函数,无论是类方法还是实例方法我们都可以获取对应的 IMP.
而 Method 将 Selector 和 IMP 联系起来,可从源码中看出:
1 | struct objc_method { |
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 | @implementation Student |
调用测试一下.
1 | - (void)viewDidLoad { |
这里 Student 并没有 walk
方法,故意为之,运行后控制台会打印:
1 | ---veryitman--- Student studentWalkImp |
成功的为 Student 添加了一个实例方法 walk
的实现 studentWalkImp
.
上面的例子是使用 OC 的 IMP 方式来实现的,可以改为 C 实现版本的.
1 | @implementation Student |
2. 动态添加类方法
动态添加类方法,和动态添加实例方法稍微有点不同。下面是改造后的 Student.m
.
Student.m
1 | @implementation Student |
注意:这里获取 Class 稍微不同的是使用了 objc_getMetaClass
,这里关系到 Objective-C 中的类、Class、根类和元类的区别,可以参考 Class、isa、元类 这篇文章。
调用测试一下:
1 | - (void)viewDidLoad { |
控制台打印:
1 | ---veryitman--- Student clsImp |
成功地为类动态的添加了一个类方法 clsImp
.
参考文档
2. Apple RunTime 源码 objc4-723.tar.gz
3. Messaging
4. Objective-C 深入理解中的消息机制和方法调用
完整代码
点击下载文中完整的 Demo.
扫码关注,你我就各多一个朋友~