什么是runtime
runtime
是由C
、C++
、汇编
一起写成的api
,为OC
提供运行时。
运行时:装载内存,提供运行时功能(依赖于
编译时:把高级语言(OC、Swift、Java等)源代码编译成能够识别的语言(机器语言-->二进制)runtime
)
底层库关系:
对象和方法的本质
int main(int argc, const char * argv[]) { @autoreleasepool { LGPerson *p = [[LGPerson alloc] init]; [p run]; } return 0;}复制代码
clang 编译,cd到相应的文件下,打开终端,输入下面命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.c++或clang -rewrite-objc main.m -o test.c++复制代码
打开生成的testMain.c++文件,很长,有几万行代码,我们看主要的,如下
可有看出,对象的本质是一个结构体,方法的本质是发送消息。任何方法的调用都可以翻译成是objc_msgSend
这个方法的调用
类方法和实例方法
对象调用
LGStudent *s = [[LGStudent alloc] init];objc_msgSend(s, sel_registerName("run"));复制代码
类方法的调用
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("run"));复制代码
向父类发消息(对象方法)
struct objc_super mySuper;mySuper.receiver = s;mySuper.super_class = class_getSuperclass([s class]);objc_msgSendSuper(&mySuper, @selector(run));复制代码
通过objc_msgSendSuper
向父类发消息,第一个参数是结构体指针(父类)
向父类发消息(类方法)
struct objc_super myClassSuper;myClassSuper.receiver = [s class]; // 当前类myClassSuper.super_class = class_getSuperclass(object_getClass([s class])); // 当前类的类 = 元类objc_msgSendSuper(&myClassSuper, @selector(run));复制代码
Runtime的三种调用方式:
1、runtime api
--> (class_、objc_、object_) 2、NSObject api
--> (isKindOfClass、isMemberOfClass) 3、OC上层方法 -->(@selector)
注意点:
对象方法存在哪? ==> 类 实例方法 类方法存在哪? ==> 元类 实例方法 类方法在元类里是什么形式存在? ==> 实例方法消息的发送Objc_msgSend
两种方式:
- 快速 缓存找-通过汇编
- 慢速
objc_msgSend 是用汇编写的,高效以及C语言不能改通过写一个函数,保留未知的参数,去跳转到任意的指针,汇编可以利用寄存器实现。
下面进入干货,源码查看如何寻找imp
,汇编部分:
上面这些汇编语言,主要就是为了寻找imp
,调用_objc_msgSend
然后判断接收者recevier
是否为空,为空则返回,不为空,就处理isa
,完毕之后就调用CacheLookup NORMAL
缓存找imp
,CacheLookup
的结果又分三种,如果找到了,则调用CacheHit
进行call or return imp
;如果是第二种CheckMiss
,则进行下一步的函数调用__objc_msgSend_uncached
;第三种是如果在别的地方找到了这imp
,那么就在这里进行add
操作,为了方便下一次快速的查找。
着重查看一下方法__objc_msgSend_uncached
的调用:
__class_lookupMethodAndLoadCache3
就会发现,在汇编层次,已经走不下去了,其实从这个方法开始,就会从汇编转到C++或者C层次的代码上了,后面继续看。 从上面代码可以看出,这是一个漫长的查找过程,先从自己的方法列表里查找,如果找到,就调用,同时把该imp
存放在缓存中;如果没有找到,就到自己的父类里查找,接着后面是一个往复的过程,递归查找父类,直到找到NSObject
这个类。
动态解析
变量triedResolver
使得动态解析只走一次。重点关注_class_resolveMethod
方法:
上面代码判断是否是元类,不是元类走_class_resolveInstanceMethod
方法,是元类走_class_resolveClassMethod
方法。
当我们重写+resolveClassMethod
和+resolveInstanceMethod
方法的时候,是如何走到那里的呢,可以通过下面源码看出
下面通过代码了解一下动态解析:
@interface LGPerson : NSObject- (void)run;@end@implementation LGPerson#pragma mark - 动态方法解析+ (BOOL)resolveInstanceMethod:(SEL)sel { NSLog(@"来了 老弟"); return [super resolveInstanceMethod:sel];}@endint main(int argc, const char * argv[]) { @autoreleasepool { [[LGPerson alloc] run]; } return 0;}复制代码
注意,上面代码中,类LGPerson
没有实现run
这个实例方法,同时父类以及分类里都没有实现,在.m
文件里重写里resolveInstanceMethod:
方法。运行代码
+ (BOOL)resolveInstanceMethod:(SEL)sel
明显走了两次,在上面源代码中,我们分析了,变量 triedResolver
使得动态解析只走一次,这里又是什么原因呢? 下面通过bt
寻找原因,在方法+ (BOOL)resolveInstanceMethod:(SEL)sel
加一个断点,看下图
_objc_msgSend_uncached
,然后走方法 lookUpImpOrForward
,在走到方法 _class_resolveInstanceMethod
里,从这个大致的流程可以知道,这个流程,就是上面所分析的流程,寻找 imp
的过程,没有找到,就走到里动态解析这一步; 下面跳过断点,第二次走到+ (BOOL)resolveInstanceMethod:(SEL)sel
方法里
如果我们在这一步进行重定向,可以使用下面的方式
+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(run)) { // 动态解析我们的 对象方法 NSLog(@"对象方法解析走这里"); SEL readSEL = @selector(readBook); Method readM= class_getInstanceMethod(self, readSEL); IMP readImp = method_getImplementation(readM); // 获取重定向方法的imp const char *type = method_getTypeEncoding(readM); return class_addMethod(self, sel, readImp, type); // 添加方法的实现 } return [super resolveInstanceMethod:sel];}复制代码
上面说的都是实例方法,下面看看类方法,通过源码可以知道,在调用方法_class_resolveClassMethod
之后,还会在调用方法_class_resolveInstanceMethod
,调用方法_class_resolveClassMethod
我们可以理解,因为是动态解析类方法,但是为什么会去调用方法_class_resolveInstanceMethod
,大家知道,这个方法是去动态解析实例方法所用的。
还记得前面说过的类方法的存放位置么?第一它是类的类方法,第二它是元类的实例方法。所以在寻找类方法的imp
的过程就多了一步,如果有疑问,可以通过下面代码验证
ip
和从元类里获取到的实例方法的 ip
是一样的。如果你还是感觉不可靠,那么也可以通过下面的方式去验证: // NSObject的分类 验证上述问题的时候,可以先后注释掉实例方法和类方法#import "NSObject+ZB.h"#import@implementation NSObject (ZB)+ (void)run { NSLog(@"NSObject === + run");}- (void)run { NSLog(@"NSObject === - run");}@end// ZBPerson继承NSObject,只在.h文件中声明里类方法run,并未去实现@interface ZBPerson : NSObject+ (void)run;@end// 直接调用类方法,同时注释掉NSObject分类里的类方法runint main(int argc, const char * argv[]) { @autoreleasepool { [ZBPerson run]; } return 0;}复制代码
按照上述描述,编译运行结果如下
看到没有,我们调用的明明是类方法run
,为什么在这里却走到了一个实例方法里面。希望小伙伴们能够好好的去体会前面说过的一句话, 类方法在元类中的存储方式是以实例方法去存储的 那么如果打开类方法run
的注释呢?看下面结果
run
,没有调用实例方法呢?因为这个过程,只要找到了 imp
就会立即调用,后面的过程也就不用在走了。 记住下面这张图,理清楚isa的走位,以及superclass的走位(如果图中标注有错误,还希望指出,谢谢)
消息转发
当动态解析并没有获取到我们想要的imp
时,它返回一个NO
,接下来会走到消息转发。
下面给出了消息转发中的三个方法的使用
#pragma mark - 消息转发- (id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"%s",__func__); if (aSelector == @selector(run)) { // 转发给我们的ZBStudent 对象 return [ZBStudent new]; } return [super forwardingTargetForSelector:aSelector];}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSLog(@"%s",__func__); if (aSelector == @selector(run)) { // forwardingTargetForSelector 没有实现 就只能方法签名了 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];}复制代码
这三个方法,相信大家已经很熟悉了,方法forwardingTargetForSelector:
允许我们替换消息的接收者为其他对象,如果这个方法返回nil
或者self
,则会向对象发送methodSignatureForSelector:
消息,获取到方法的签名用于生成NSInvocation
对象,最后会进入消息转发机制forwardInvocation:
,不然将返回对象重新发送消息。
配合下面的图,以上就是完整的消息转发
很多的应用也在这一层去实现的,不过现在不讨论这个,我们主要看这三个方法是如何来的,那么就继续去查看我们的源码
在源码中查找方法_objc_msgForward_impcache
的实现会发现,它又走到了汇编里,然而这部分只有汇编调用,没有源码实现,也就是没有开源。 那么又是如何知道,消息转发的过程中调用了上面所说的三个方法呢?
介绍一个方法instrumentObjcMessageSends
extern void instrumentObjcMessageSends(BOOL);int main(int argc, const char * argv[]) { @autoreleasepool { instrumentObjcMessageSends(YES); [ZBPerson run]; instrumentObjcMessageSends(NO); } return 0;}复制代码
方法instrumentObjcMessageSends
就是打印当前调用方法的调用过程,编译完成后可以在路径Macintosh HD/private/tmp/msgSends-xxxxx
下查看文件msgSends-xxxxx
,如下图