笔记-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源码解析之让你彻底了解底层源码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

大数据之眼

大数据之眼

[德]尤夫娜·霍夫施泰特 / 陈巍 / 浙江文艺出版社 / 2018-5-7 / 68.00元

德国狂销10万册的大数据商业应用畅销书,经典之作《大数据时代》的姊妹篇。 该书在德语国家促发了一场关于大数据,人工智能与人的关系建构的大讨论。 德国大数据与人工智能领域权威,首度为中国读者亲笔作序。 在后大数据时代,如何维护自己的隐私,如何巧妙利用资源获得更多金钱? 一部对大数据发展所产生的问题进行思考和规避的先知式作品。 当智能机器欲“优化”我们,入侵我们的生活,统......一起来看看 《大数据之眼》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具