Runtime底层原理分析之objc_megSend

一、前言

最近我想要研究一下 Runtime 的底层原理,于是下载了一份 runtime 的源码,学习的过程中也查阅了很多资料,询问了很多大牛。现在总结一下我的收获。
Runtime 是一套由 CC++、汇编写成的为 OC 提供运行时机制的东西。Runtime 的源码可以在苹果的官网 opensource 下载到,我下载的是当时的最新版本 objc4-750点击此地址可以去下载

1802326-cc03a85cef30aabf.png
objc4-750.png

二、IMP 和 objc_msgSend

1、SEL 和 IMP

首先我们先认识一下 SELIMP

1802326-8cf44822b6411f49.png
IMP.png

从下载的 runtime 源码中我们可以看到 IMP 的定义:
“IMP是指向函数具体实现的指针”
这个函数体前两个参数是 id(消息接收者,也就是对象),以及 SEL(方法的名字)。
类比:书的目录(SEL)——页码(IMP:指向函数具体实现的指针)——具体内容(函数实现)
那么 SEL 是如何找到 IMP 的呢?

例如:有一个 Person 类,初始化一个 xiaoming 对象。

1802326-d4995597a723936c.png
Person.png

然后在 study 方法执行之前打一个断点,并运行,当运行到断点处,我们打开汇编调试模式,步骤如下 Debug—> Debug Workflow—>Always Show Disassembly

1802326-f28979e74466c68b.png
debug汇编模式.png

我们就来到了一个汇编的界面,如下图,观察可看到在allocinitstudy后面都有 objc_msgSend 函数的调用。
注意:在模拟器上运行才能看到右侧那些alloc、init、study函数名的打印,真机上是看不到的)

1802326-19478ef7bb087d92.png
objc_msgSend函数.png

每个方法都调用了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_timp、哈希表。
  • 2、慢速查找:属于 C/C++ 部分。

从源码中我们可以找到 objc_class,可以得知,每个类都有一个缓存 cache,存放着方法的 selimpselimp 最终会组成一张哈希表,这样通过 sel 可以快速的查找到 imp,所以当我们查找一个方法的时候,首先查找的就是这个 cache

1802326-811b8783a29573ef.png
cache.png

1、快速查找部分(汇编部分)

1802326-1c51519019db888a.png
汇编部分快速查找.png

这部分属于汇编部分,涉及了汇编语言的语法,虽然我不熟悉汇编语言,但是还是能够找到一些关键的地方,理解整个流程的走向。

首先,在下载好的 runtime 源码中搜索 _objc_msgSend,选择查看 arm64 下的,也就是 objc-msg_arm64.s 如图:

1802326-1581397e0039822c.png
_objc_msgSend.png

通过sel找到imp:
然后在 objc-msg_arm64.s 中搜索查看 ENTRY _objc_msgSend
流程首先执行的是 ENTRY _objc_msgSend

1802326-59a11d74aafa5260.png
_objc_msgSend流程.png

然后再进行 LNilOrTagged 判断,
判断是否为 nil 或者是否支持 tagged_pointers 类型,
nil 或者不支持就走 LRetrunZero,执行 END_ENTRY _objc_msgSend 结束。

1802326-3e5901f767eee7ac.png
LReturnZero.png

如果不为 nil 并且支持,就走 LGetIsaDone 执行完毕

1802326-20eda13e845d3137.png
LGetIsaDone.png

接下来在 LGetIsaDone 里执行 CacheLookup NORMAL(从缓存里找imp)
如果有缓存,则直接 calls imp,否则执行 objc_msgSend_uncached
下面我们来看看CacheLookup,它是一个宏,如果在缓存里找到了,则执行CacheHit,没找到执行CheckMiss,没找到但是在其他地方已经找到了,可以add添加进缓存里去。

1802326-09f8740e685239e1.png
CacheLookup.png
CacheHitCheckMiss也分别都是宏,里面有相应的操作,再次就不深入细讲了。我们只要知道,如果没有找到, CheckMiss中执行的是 __objc_msgSend_uncached方法。
1802326-9bf362fe87db0423.png
__objc_msgSend_uncached详细.png

MethodTableLookup又是个宏,这是个方法列表,也是个重点核心。

1802326-98a91f99053c58b3.png
MethodTableLookup.png

我们至此,发现搜索不到__class_lookupMethodAndLoadCache3,此时可能会失去信心,但不要担心,我们能搜索到_class_lookupMethodAndLoadCache3
_class_lookupMethodAndLoadCache3C++ 中的函数,从而从汇编开始到 C++ 中了。

接下来看 objc-runtime-new.mm 中的_class_lookupMethodAndLoadCache3

1802326-f2128b2c46bf836b.png
_class_lookupMethodAndLoadCache3.png

快速查找(汇编阶段)到此完毕!
因为快速查找没找到,所以 慢速查找部分(C/C++部分)开始。

2、慢速查找部分(C/C++部分)

1802326-cca81468c63aa14a.png
慢速查找部分.png

现在首先看 当前类 的缓存里现在有没有了,如果有,则返回 imp 。如果没有,则执行getMethodNoSuper_nolock,在当前类查找,如果找到了,则执行 log_and_fill_cache,把 imp 存到缓存中去,并返回要查找的 imp。这样下次再找的时候,就会直接进行汇编快速查找,直接CacheHit了。

1802326-5feb4973a6f7cef8.png
tryThisClassCache.png

如果当前类也没有,则查找当前类的父类,对父类进行 for 循环,因为最终的父类都是 NSObjectNSObject 的父类则是 nil 了,所以我们只遍历到 NSObject。如果父类里有缓存,那么通用把 imp 存到缓存中去,并返回要查找的 imp。如果没有缓存,就执行getMethodNoSuper_nolock

1802326-469567e28bbc9759.jpg
findsuper.jpg

如果父类中也没有,那么就开始下一个步骤,动态方法解析

3、动态方法解析 和 消息转发

1)消息转发流程简述

当一个方法没有找到的时候,会经历几个步骤才会崩溃,先是经过动态方法解析步骤,如果消息还未得到处理,则进入forwardingTargetForSelector:,还未处理则进入methodSignatureForSelector:forwardInvocation
下面是一个消息转发流程的简图。

1802326-2957af10e2361501.png
消息转发流程.png

在这几个步骤我们都可以设法拦截崩溃信息,处理未处理的消息。我们可以对 crash 进行自定义处理,防止崩溃的发生。也可以把 crash 收集起来发给服务器。

2)动态方法解析

下面详细看一下动态方法解析的具体流程,首先如果父类中没有找到 imp,那么开始进行动态方法解析,执行_class_resolveMethod。由于传进来的参数 resolverYEStriedResolver 默认第一次是NO,可以进入判断,但是只会调用一次,调用过后就会把 triedResolver 设为 YES

1802326-fc75f68564fa26c6.png
动态方法解析.png

_class_resolveMethod方法中,判断如果是 元类,则执行_class_resolveInstanceMethod,否则执行_class_resolveClassMethod之后再执行_class_resolveInstanceMethod

问:
为什么执行完_class_resolveClassMethod之后会再次执行一次_class_resolveInstanceMethod
答:
比如有一个类 Person,我们查找 Person 的一个类方法,如果没找到,会继续找他的第一个 元类,再找不到,会继续找 根元类 ,最终会找到 NSObject
Person(类方法) 找——> 元类(实例方法) 找——> 根元类(实例方法) 找——> NSObject(实例方法)
实例方法存在类对象里面,类方法存在元类里面。

1802326-914c3acc127bb0e0.png
屏幕快照 2019-05-22 下午6.38.39.png

所以最终,还会执行一次 _class_resolveInstanceMethod

1802326-0fc86901c56967aa.png
_class_resolveMethod.png

_class_resolveInstanceMethod的内部实现,实质就是 消息的发送

1802326-ec2fcfda29f3fbae.png
_class_resolveInstanceMethod.png

例如我们调用了 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方法

1802326-a3d02b72a5f9710e.png
unrecognized.png
这个我们常见的 unrecognized selector 错误信息其实是由这个 objc_defaultForwardHandler函数打印的
1802326-9a5fd9aceadceb0a.png
objc_defaultForwardHandler.png

以上就是我对 Runtime 的消息转发objc_megSend 的一个主要的底层原理分析总结。如果有写错的地方,还请帮忙指出,多谢,互相进步。

以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享!:
1、《Objective-C 2.0运行时系统编程指南》
2、作者黄文臣的《iOS Runtime详解之SEL,Class,id,IMP,_cmd,isa,method,Ivar》

转载请备注原文出处,不得用于商业传播——凡几多

转载于:https://www.jianshu.com/p/0d6dfdb20318

猜你喜欢

转载自blog.csdn.net/weixin_33704591/article/details/91063383