初探objc_msgSend

前言

之前探索了cache_t的底层结构和一些方法。了解到了cache_t的插入流程,那么这个insert流程是在哪里调起的呢,一起来探索一下。

探索怎么调用insert()

首先找到在哪里调用了insert()方法,全局搜索.insert(

image.png

找到了两个地方调用了,分别是lookupMethodInClassAndLoadCache()log_and_fill_cache()

lookupMethodInClassAndLoadCache()

image.png
这里有注释,大致意思是这个方法不完整,没有解析和构造程序,但是它只用于.cxx_construct.cxx_destruct,所以没关系。
如此看来这个函数不是我们想找的,但是我还是特别好奇什么时候会调用它,同样全局搜索,发现分别是object_cxxConstructFromClass()object_cxxDestructFromClass()在调用,顺着这两个方法最终找到了alloc()dealloc()
可以看出这个方法就是在构造和析构的时候才调动的。但是通过断掉调试的时候发现alloc和dealloc都没有走到这个方法来。

image.png
比如dealloc方法,要进入object_dispose()才有可能进入lookupMethodInClassAndLoadCache()
我们把对象做了一个弱引用,就进入了object_dispose(),但是后面的方法里还有has_cxx_dtor判断。这个暂时还不知道怎么弄,就这样吧,只要满足条件,应该是会进入lookupMethodInClassAndLoadCache()的。

log_and_fill_cache()

通过全局搜索,最终只找到了class_getInstanceMethodclass_getClassMethod,看到这两个函数想必大家应该比较熟悉了。但是这个并不是我们的目的,我们是想找到系统是怎么调用insert()的。所以得想其他办法了。

查找到objc_msgSend

image.png

既然找不到,还是回到原点,再看看有没有什么线索,最终在objc-cache.mm文件里,我们注意到了这样一段注释,下面是读/写,有几个方法的使用,这里的方法我们基本都研究过了,上面是读,分别是objc_msgSendcache_getImp,这两个之前没研究过,那或者重点就在这里了。

objc_msgSend

runtime

首先来看看编译时是什么,编译时顾名思义就是正在编译的时候。那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。)。那编译时就是简单的作一些翻译工作,比如检查有没有写错啥关键字了啊,有啥词法分析,语法分析之类的过程,如果发现啥错误编译器就告诉你,这时的错误就叫编译时错误。这个过程中做的类型检查也就叫编译时类型检查,或静态类型检查(所谓静态,就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。所以有时一些人说编译时还分配内存啥的肯定是错误的说法。

运行时就是代码跑起来了,被装载到内存中去了。
(你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内存中才变成活的)。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。

Runtime有两个版本 一个Legacy版本(早期版本) ,一个Modern版本(现行版本)

  • 早期版本对应的编程接口:Objective-C 1.0,用于Objective-C 1.0, 32位的Mac OS X的平台上
  • 现行版本对应的编程接口:Objective-C 2.0,用于iPhone程序和Mac OS X v10.5 及以后的系统中的 64 位程序

runtime官方文档:Objective-C Runtime Programming Guide

runtime有三种调用方式,分别是:

  • Objective-C Code,比如 [obj sayNB]
  • Framework&Serivce,比如 isKindofClass
  • Runtime API,比如 class_getInstanceSize

方法调用的本质

通过查看方法的底层代码,看看是怎么调用的。
image.png
OC代码
image.png
c++代码
可以看到方法调用的本质就是发送消息,上面有是两种调用obj_msgSend消息发送的方式,一种是方法不带参数,一种是方法带参数。

  • 第一个参数:id – 方法调用的对象,即消息的接收者;
  • 第二个参数:SEL – 调用的方法,带参数的sel有”:”,不带参数的则没有;
  • 第三个参数或更多:参数 – 方法传入的参数,一个或者更多。

objc_msgSend底层源码

objc_msgSend是运行时runtime实现的,并且是使用汇编语言来完成的,为什么会用汇编呢,是因为汇编语言所编写的程序具有存储空间占用少、执行速度快的特点,而消息发送正需要这样的特点。
开始查找底层代码,直接在源码中全局搜索。

image.png

搜索出来的结果很多,但因为是汇编写的,所以直接看.s文件,可以看到都是objc-msg,只是版本不同,那我们就直接看arm64版本的。
根据搜索找到ENTRY _objc_msgSend,这里就是源码所在。

image.png

本人并没有学习过汇编,只能通过查看资料来一句一句的了解其中的意思。

UNWIND _objc_msgSend, NoFrame
UNWIND : 它的原理是记录每个函数的入栈指令到特殊的段。
#define NoFrame 0x02000000
这一句应该是记录_objc_msgSend到这个地址。

cmp p0, #0 // nil check and tagged pointer check
cmp : 比较.(两操作数作减法,仅修改标志位,不回送结果). 。
将p0和#0,进行比较。

#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
如果是64位SUPPORT_TAGGED_POINTERS = 1,所以是进入b.le LNilOrTagged

b.le LNilOrTagged
b.le :判断上面cmp的值是小于等于,如果满足执行标号,否则直接往下走。
我们传了有值进来所以不满足,往下走。

ldr p13, [x0] // p13 = isa
是指 p13 = x0栈中的值,而x0在一系列的赋值后就是消息接收者的isa。
而x0来自ldp x0, x1, [sp, #(8*16+0*8)]
这一句是指将sp偏移8*16个字节的值取出来,放入x1 和 x0

延伸:栈就是指令执行时存放临时变量的内存空间,具有特殊的访问方式:后进先出, Last In Out Firt。
栈是从高地址到低地址存储数据的,栈底是高地址,栈顶是高地址。
FP指向栈底
SP指向栈顶

GetClassFromIsa_p16 p13, 1, x0 // p16 = class
这里是指调用 GetClassFromIsa_p16
下面分别是LGetIsaDoneLNilOrTaggedLReturnZero,这里面的内容过多,我们下一回再说。那么先开看看GetClassFromIsa_p16里面做了什么。

GetClassFromIsa_p16

image.png
首先判断是否SUPPORT_INDEXED_ISA,不是,然后再判断是否64位,是64位的,再判断是否\needs_auth,这个是外面传来的值,传入的是1,所以应该进入else,
即调用ExtractISA p16, \src, \auth_address

.macro ExtractISA
	and    $0, $1, #ISA_MASK
.endmacro
// GetClassFromIsa_p16 p13, 1, x0
// ExtractISA p16, \src, \auth_address
// 这里的操作主要是将x0与上p13,最后赋值给p16。
复制代码

这一步就获取到了消息接收者的class,为什么消息发送要获取到class呢,主要是因为实例对象的cache就是存送在class中。

小结

这一段objc_msgSend的源码差不多就这些,大致就是检查接收者是否为nil,再通过isa拿到class。可是这并没有和cache_t联系上,想要知道后面怎么操作的,请听下回分解。

引用:isb 汇编_汇编基础 汇编指令

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享