一、前言
最近我想要研究一下 Runtime 的底层原理,于是下载了一份 runtime 的源码,学习的过程中也查阅了很多资料,询问了很多大牛。现在总结一下我的收获。
Runtime 是一套由 C、C++、汇编写成的为 OC 提供运行时机制的东西。Runtime 的源码可以在苹果的官网 opensource 下载到,我下载的是当时的最新版本 objc4-750,点击此地址可以去下载。
二、IMP 和 objc_msgSend
1、SEL 和 IMP
首先我们先认识一下 SEL 和 IMP。
从下载的 runtime 源码中我们可以看到 IMP 的定义:
“IMP是指向函数具体实现的指针”。
这个函数体前两个参数是 id(消息接收者,也就是对象),以及 SEL(方法的名字)。
类比:书的目录(SEL)——页码(IMP:指向函数具体实现的指针)——具体内容(函数实现)
那么 SEL 是如何找到 IMP 的呢?
例如:有一个 Person 类,初始化一个 xiaoming 对象。
然后在 study 方法执行之前打一个断点,并运行,当运行到断点处,我们打开汇编调试模式,步骤如下 Debug—> Debug Workflow—>Always Show Disassembly。
我们就来到了一个汇编的界面,如下图,观察可看到在alloc
、init
和study
后面都有 objc_msgSend
函数的调用。
(注意:在模拟器上运行才能看到右侧那些alloc、init、study函数名的打印,真机上是看不到的)
每个方法都调用了objc_msgSend
函数,很有意思不是吗?但当我们在工程里想搜索objc_msgSend
看一下里面什么样时却发现没有,这时我们下载的 runtime 源码就派上用场了。
2、objc_msgSend
说到 Runtime ,就不能不提 objc_msgSend
消息转发。
在 Objective-C 中,消息是直到运行的时候才和方法实现绑定的。编译器会把一个消息表达式,
[receiver message]
转换成一个对消息函数 objc_msgSend
的调用。
该函数有两个主要参数:消息接收者 receiver 和消息对应的方法名字 selector ——也就是方法选标:
objc_msgSend(receiver, selector)
可同时接收消息中的任意数目的参数:
objc_msgSend(receiver, selector, arg1, arg2, ...)
该消息函数做了动态绑定所需要的一切:
- 它首先找到选标所对应的方法实现。因为不同的类对同一方法可能会有不同的实现,所以找到的 方法实现依赖于消息接收者的类型。
- 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传给找到的方法实现。
- 最后,将方法实现的返回值作为该函数的返回值返回
三、objc_msgSend 源码分析流程
接下来我们在 Runtime 中跟踪一下 objc_msgSend 的整个流程。
objc_msgSend 查找分为两种方式:
- 1、快速查找:在缓存找,属于汇编部分,cache_t、imp、哈希表。
- 2、慢速查找:属于 C/C++ 部分。
从源码中我们可以找到 objc_class,可以得知,每个类都有一个缓存 cache,存放着方法的 sel 和 imp,sel 和 imp 最终会组成一张哈希表,这样通过 sel 可以快速的查找到 imp,所以当我们查找一个方法的时候,首先查找的就是这个 cache。
1、快速查找部分(汇编部分)
这部分属于汇编部分,涉及了汇编语言的语法,虽然我不熟悉汇编语言,但是还是能够找到一些关键的地方,理解整个流程的走向。
首先,在下载好的 runtime 源码中搜索 _objc_msgSend,选择查看 arm64 下的,也就是 objc-msg_arm64.s 如图:
通过sel找到imp:
然后在 objc-msg_arm64.s 中搜索查看 ENTRY _objc_msgSend,
流程首先执行的是 ENTRY _objc_msgSend
然后再进行 LNilOrTagged 判断,
判断是否为 nil 或者是否支持 tagged_pointers 类型,
为 nil 或者不支持就走 LRetrunZero,执行 END_ENTRY _objc_msgSend 结束。
如果不为 nil
并且支持,就走 LGetIsaDone
执行完毕
接下来在 LGetIsaDone
里执行 CacheLookup NORMAL
(从缓存里找imp)
如果有缓存,则直接 calls imp
,否则执行 objc_msgSend_uncached
。
下面我们来看看CacheLookup
,它是一个宏,如果在缓存里找到了,则执行CacheHit
,没找到执行CheckMiss
,没找到但是在其他地方已经找到了,可以add
添加进缓存里去。
CacheHit
和CheckMiss
也分别都是宏,里面有相应的操作,再次就不深入细讲了。我们只要知道,如果没有找到,CheckMiss
中执行的是__objc_msgSend_uncached
方法。MethodTableLookup
又是个宏,这是个方法列表,也是个重点核心。
我们至此,发现搜索不到__class_lookupMethodAndLoadCache3
,此时可能会失去信心,但不要担心,我们能搜索到_class_lookupMethodAndLoadCache3
,
_class_lookupMethodAndLoadCache3
是 C++ 中的函数,从而从汇编开始到 C++ 中了。
接下来看 objc-runtime-new.mm 中的_class_lookupMethodAndLoadCache3
快速查找(汇编阶段)到此完毕!
因为快速查找没找到,所以 慢速查找部分(C/C++部分)开始。
2、慢速查找部分(C/C++部分)
现在首先看 当前类 的缓存里现在有没有了,如果有,则返回 imp 。如果没有,则执行getMethodNoSuper_nolock
,在当前类查找,如果找到了,则执行 log_and_fill_cache
,把 imp 存到缓存中去,并返回要查找的 imp。这样下次再找的时候,就会直接进行汇编快速查找,直接CacheHit
了。
如果当前类也没有,则查找当前类的父类,对父类进行 for 循环,因为最终的父类都是 NSObject ,NSObject 的父类则是 nil 了,所以我们只遍历到 NSObject。如果父类里有缓存,那么通用把 imp 存到缓存中去,并返回要查找的 imp。如果没有缓存,就执行getMethodNoSuper_nolock
。
如果父类中也没有,那么就开始下一个步骤,动态方法解析。
3、动态方法解析 和 消息转发
1)消息转发流程简述
当一个方法没有找到的时候,会经历几个步骤才会崩溃,先是经过动态方法解析步骤,如果消息还未得到处理,则进入forwardingTargetForSelector:
,还未处理则进入methodSignatureForSelector:
和forwardInvocation
。
下面是一个消息转发流程的简图。
在这几个步骤我们都可以设法拦截崩溃信息,处理未处理的消息。我们可以对 crash 进行自定义处理,防止崩溃的发生。也可以把 crash 收集起来发给服务器。
2)动态方法解析
下面详细看一下动态方法解析的具体流程,首先如果父类中没有找到 imp,那么开始进行动态方法解析,执行_class_resolveMethod
。由于传进来的参数 resolver 是 YES
,triedResolver 默认第一次是NO
,可以进入判断,但是只会调用一次,调用过后就会把 triedResolver 设为 YES
。
在_class_resolveMethod
方法中,判断如果是 元类,则执行_class_resolveInstanceMethod
,否则执行_class_resolveClassMethod
之后再执行_class_resolveInstanceMethod
。
问:
为什么执行完_class_resolveClassMethod
之后会再次执行一次_class_resolveInstanceMethod
?
答:
比如有一个类 Person,我们查找 Person 的一个类方法,如果没找到,会继续找他的第一个 元类,再找不到,会继续找 根元类 ,最终会找到 NSObject:
Person(类方法) 找——> 元类(实例方法) 找——> 根元类(实例方法) 找——> NSObject(实例方法)
实例方法存在类对象里面,类方法存在元类里面。
所以最终,还会执行一次_class_resolveInstanceMethod
。
而_class_resolveInstanceMethod
的内部实现,实质就是 消息的发送。
例如我们调用了 Person 类的对象方法 run,但 Person.m 没有实现 run 方法,并且父类也没有,那么我们就开启下面的动态方法解析,
重写resolveInstanceMethod
来动态解析对象方法,
重写resolveClassMethod
来动态解析类方法。
// .m没有实现,并且父类也没有,那么我们就开启下面的动态方法解析
#pragma mark - 动态方法解析
#import "LGPerson.h"
#include <objc/runtime.h>
@implementation Person
//- (void)run{
// NSLog(@"%s",__func__);
//}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
// 我们动态解析我们的 对象方法
NSLog(@"对象方法解析走这里");
SEL readSEL = @selector(readBook);
Method readM= class_getInstanceMethod(self, readSEL);
IMP readImp = method_getImplementation(readM);
const char *type = method_getTypeEncoding(readM);
return class_addMethod(self, sel, readImp, type);
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(walk)) {
// 我们动态解析我们的 类方法
NSLog(@"类方法解析走这里");
SEL hellowordSEL = @selector(helloWord);
// 类方法就存在我们的元类的方法列表
// 类 类犯法
// 元类 对象实例方法
Method hellowordM1= class_getClassMethod(self, hellowordSEL);
Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
IMP hellowordImp = method_getImplementation(hellowordM);
const char *type = method_getTypeEncoding(hellowordM);
NSLog(@"%s",type);
return class_addMethod(object_getClass(self), sel, hellowordImp, type);
}
return [super resolveClassMethod:sel];
}
如果动态解析步骤也没有找到解决办法,那么再进行到下一步骤。消息转发。
3)消息转发
<1>、forwardingTargetForSelector
方法:
比如我们除了 Person 类,还有一个 Dog 类,这个类里才有 run 方法,那么当我们判断到是 run 方法找不到时,就可以把消息转发给 Dog 类。
#pragma mark - 消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(run)) {
// 转发给我们的 Dog 对象
return [Dog new];
}
return [super forwardingTargetForSelector:aSelector];
}
<2>、methodSignatureForSelector
方法:
如果forwardingTargetForSelector没有拦截住,那么只能用最后一道关卡methodSignatureForSelector
了,获取方法签名,然后移交给消息转发forwardInvocation
。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(run)) {
// 获取方法签名
Method method = class_getInstanceMethod(object_getClass(self), @selector(readBook));
const char *type = method_getTypeEncoding(method);
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];// 移交给消息转发
}
return [super methodSignatureForSelector:aSelector];
}
// 消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
NSLog(@"------%@-----",anInvocation);
anInvocation.selector = @selector(readBook);
[anInvocation invoke];
}
如果以上所有步骤都没有把消息成功处理,那么就会崩溃了。比如没有找到study方法
objc_defaultForwardHandler
函数打印的以上就是我对 Runtime 的消息转发objc_megSend
的一个主要的底层原理分析总结。如果有写错的地方,还请帮忙指出,多谢,互相进步。
以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享!:
1、《Objective-C 2.0运行时系统编程指南》
2、作者黄文臣的《iOS Runtime详解之SEL,Class,id,IMP,_cmd,isa,method,Ivar》
转载请备注原文出处,不得用于商业传播——凡几多