内容简介:运行时:装载内存,提供运行时功能(依赖于编译时:把高级语言(OC、Swift、Java等)源代码编译成能够识别的语言(机器语言-->二进制)
runtime 是由 C 、 C++ 、 汇编 一起写成的 api ,为 OC 提供运行时。
运行时:装载内存,提供运行时功能(依赖于 runtime )
编译时:把高级语言(OC、Swift、 Java 等)源代码编译成能够识别的语言(机器语言-->二进制)
底层库关系:
对象和方法的本质
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];
}
@end
int 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 <objc/runtime.h>
@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分类里的类方法run
int 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 ,如下图
以上所述就是小编给大家介绍的《笔记-runtime源码解析之让你彻底了解底层源码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 深度解读 ReentrantLock 底层源码
- 浅谈 Java 集合 | 底层源码解析
- Colly源码解析——结合例子分析底层实现
- 直面底层:“吹上天”的协程,带你深入源码分析
- 「Go」- golang源码分析 - channel的底层实现
- Java8线程池ThreadPoolExecutor底层原理及其源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First Web Design
Ethan Watrall、Jeff Siarto / O’Reilly Media, Inc. / 2009-01-02 / USD 49.99
Want to know how to make your pages look beautiful, communicate your message effectively, guide visitors through your website with ease, and get everything approved by the accessibility and usability ......一起来看看 《Head First Web Design》 这本书的介绍吧!