笔记-runtime源码解析之让你彻底了解底层源码

栏目: 编程语言 · 发布时间: 5年前

内容简介:运行时:装载内存,提供运行时功能(依赖于编译时:把高级语言(OC、Swift、Java等)源代码编译成能够识别的语言(机器语言-->二进制)

runtime 是由 CC++汇编 一起写成的 api ,为 OC 提供运行时。

运行时:装载内存,提供运行时功能(依赖于 runtime

编译时:把高级语言(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++文件,很长,有几万行代码,我们看主要的,如下

笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码

可有看出, 对象的本质是一个结构体方法的本质是发送消息 。任何方法的调用都可以翻译成是 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 ,汇编部分:

笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码

上面这些汇编语言,主要就是为了寻找 imp ,调用 _objc_msgSend 然后判断接收者 recevier 是否为空,为空则返回,不为空,就处理 isa ,完毕之后就调用 CacheLookup NORMAL 缓存找 impCacheLookup 的结果又分三种,如果找到了,则调用 CacheHit 进行 call or return imp ;如果是第二种 CheckMiss ,则进行下一步的函数调用 __objc_msgSend_uncached ;第三种是如果在别的地方找到了这 imp ,那么就在这里进行 add 操作,为了方便下一次快速的查找。

着重查看一下方法 __objc_msgSend_uncached 的调用:

笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码
玩过源码的小伙伴,走到这里方法 __class_lookupMethodAndLoadCache3

就会发现,在汇编层次,已经走不下去了,其实从这个方法开始,就会从汇编转到C++或者C层次的代码上了,后面继续看。

笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码

从上面代码可以看出,这是一个漫长的查找过程,先从自己的方法列表里查找,如果找到,就调用,同时把该 imp 存放在缓存中;如果没有找到,就到自己的父类里查找,接着后面是一个往复的过程,递归查找父类,直到找到 NSObject 这个类。

笔记-runtime源码解析之让你彻底了解底层源码

如果这个过程方法还没有查找到,那就进入动态解析的过程。

动态解析

笔记-runtime源码解析之让你彻底了解底层源码

变量 triedResolver 使得动态解析只走一次。重点关注 _class_resolveMethod 方法:

笔记-runtime源码解析之让你彻底了解底层源码

上面代码判断是否是元类,不是元类走 _class_resolveInstanceMethod 方法,是元类走 _class_resolveClassMethod 方法。

当我们重写 +resolveClassMethod+resolveInstanceMethod 方法的时候,是如何走到那里的呢,可以通过下面源码看出

笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码

下面通过代码了解一下动态解析:

@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: 方法。运行代码

笔记-runtime源码解析之让你彻底了解底层源码
可以发现, + (BOOL)resolveInstanceMethod:(SEL)sel 明显走了两次,在上面源代码中,我们分析了,变量 triedResolver

使得动态解析只走一次,这里又是什么原因呢?

下面通过 bt 寻找原因,在方法 + (BOOL)resolveInstanceMethod:(SEL)sel 加一个断点,看下图

笔记-runtime源码解析之让你彻底了解底层源码
这是第一次来到这个方法里,看一下红色框里的内容,先走方法 _objc_msgSend_uncached ,然后走方法 lookUpImpOrForward ,在走到方法 _class_resolveInstanceMethod 里,从这个大致的流程可以知道,这个流程,就是上面所分析的流程,寻找 imp

的过程,没有找到,就走到里动态解析这一步;

下面跳过断点,第二次走到 + (BOOL)resolveInstanceMethod:(SEL)sel 方法里

笔记-runtime源码解析之让你彻底了解底层源码

熟悉消息转发流程的小伙伴们或许已经看的很明白了,第二次走到这里,是在消息转发的过程中走过来的,走到这里的前提就是动态解析失败了。具体的流程会在消息转发的过程中说到。

如果我们在这一步进行重定向,可以使用下面的方式

+ (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 的过程就多了一步,如果有疑问,可以通过下面代码验证

笔记-runtime源码解析之让你彻底了解底层源码
从上面的代码可以看出,获取类的类方法得到的 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;
}

复制代码

按照上述描述,编译运行结果如下

笔记-runtime源码解析之让你彻底了解底层源码
看到没有,我们调用的明明是类方法 run ,为什么在这里却走到了一个实例方法里面。希望小伙伴们能够好好的去体会前面说过的一句话, 类方法在元类中的存储方式是以实例方法去存储的

那么如果打开类方法 run 的注释呢?看下面结果

笔记-runtime源码解析之让你彻底了解底层源码
为什么只调用了类方法 run ,没有调用实例方法呢?因为这个过程,只要找到了 imp

就会立即调用,后面的过程也就不用在走了。

记住下面这张图,理清楚isa的走位,以及superclass的走位(如果图中标注有错误,还希望指出,谢谢)

笔记-runtime源码解析之让你彻底了解底层源码

消息转发

当动态解析并没有获取到我们想要的 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: ,不然将返回对象重新发送消息。

配合下面的图,以上就是完整的消息转发

笔记-runtime源码解析之让你彻底了解底层源码

很多的应用也在这一层去实现的,不过现在不讨论这个,我们主要看这三个方法是如何来的,那么就继续去查看我们的源码

笔记-runtime源码解析之让你彻底了解底层源码
笔记-runtime源码解析之让你彻底了解底层源码
在源码中查找方法 _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源码解析之让你彻底了解底层源码

以上所述就是小编给大家介绍的《笔记-runtime源码解析之让你彻底了解底层源码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

创业时, 我们在知乎聊什么?

创业时, 我们在知乎聊什么?

知乎 / 中信出版社 / 2014-1 / 42.00元

★前所未有的互联网出版实验,500万知友亲手甄选内容,知乎三年创业问答精华大集结 ★史上最真诚创业书,用互联网思维讲透创业的逻辑 ★在知乎,最强大互联网创业群体真实分享创业路上的荣耀与隐忧 ★从Idea到步入正轨,创业公司如何招人、做技术、卖产品、找融资、建团队、处理法务? 他们在知乎聊创业: 创新工场创始人李开复 天使投资人 徐小平 小米科技创始人 雷军......一起来看看 《创业时, 我们在知乎聊什么?》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具