Runtime底层原理分析之objc_msgSend

一、前言

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

objc4-750.png

二、IMP 和 objc_msgSend

1、SEL 和 IMP

首先我们先认识一下 SELIMP

IMP.png

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

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

Person.png

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

debug汇编模式.png

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

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

cache.png

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

汇编部分快速查找.png

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

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

_objc_msgSend.png

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

_objc_msgSend流程.png

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

LReturnZero.png

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

LGetIsaDone.png

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

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

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

MethodTableLookup.png

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

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

_class_lookupMethodAndLoadCache3.png

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

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

慢速查找部分.png

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

tryThisClassCache.png

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

findsuper.jpg

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

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

1)消息转发流程简述

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

消息转发流程.png

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

2)动态方法解析

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

动态方法解析.png

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

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

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

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

_class_resolveMethod.png

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

_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方法

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容