iOS objc_msgSend 源码流程

1,463 阅读6分钟

封面图.jpg

开篇

好好学习,不急不躁。

上篇文章cache分析,讲述了类的cache,cache_t内部结构分析,cache数据插入流程;但在什么时候开始插入数据,什么场景触发数据插入,都是未知的。所以今天就讲讲,是如何触发数据插入;

cache 触发数据写入

在 insert 方法处,打断点,通过断点流程,可以看到,触发 cache::insert 方法,是由log_and_fill_cache()这个方法来触发的;

截屏2021-07-18 上午11.21.12.png

log_and_fill_cache(Class cls, **IMP** imp, **SEL** sel, **id** receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

log_and_fill_cache内部,调用了cache.insert()方法;顺着方法的执行顺序,最终看到,是由objc_msgSend发出的。现在就带领大家,进入新领域,objc_msgSend;

Runtime

Runtime

在讲述 objc_msgSend前,需要了解什么是Runtime;因为objc_msgSend是在Runtime中执行的;这里稍微简单讲解一下Runtime,后续再出文章,针对Runtime具体讲解;

都知道Objective-C是一门动态的语言。OC将源码转换为可执行的文件,需要经过三个步骤:编译、链接、运行;

编译阶段:是无法知道函数调用,或者是变量的具体类型,编译阶段,只负责将代码转换成机器能识别的语言,不负责内存的分配;

运行阶段:把需要用到的方法,变量等,从磁盘装载到内存中; 只有进入内存,程序才能跑起来; 底层是通过dyld链接的形式,将代码装载到内存中;而Runtime就是运行时的机制的基础;

Runtime 调用方式

1、通过Objective-C方法,调用Runtime相关方法;

2、通过NSObject,调用Runtime相关方法;

3、使用底层objc提供的API来发起;

runtime发起底层原理.png

Runtime API 调用原理

在之前的文章中说过,可以通过.cpp文件,查看OC代码的源码执行逻辑,所以今天我们也使用.cpp文件,来查看Runtime 底层方法是如何调用的;

在项目中,执行-(void)sayNB 方法,查看sayNB方法,如何调用的;并生成.cpp文件的方式;

clang.png

在.cpp文件中,查看 main()方法,可以看到,在编译底层,OC上层的代码都被编译为C++执行,并且是使用 Runtime的方式执行;

obj.png

OC的方法,都转换为objc_msgSend函数调用,传递一个消息接收者,和消息体,objc_msgSend(消息接收者,消息体)消息体包含:sel,参数

msg.png

所以,执行-(void)sayNB方法,与直接调用objc_msgSend(person,sel_registerName("sayNB"))一样;

由此可以看出,Runtime具备运行时功能,而OC方法的本质,其实就是消息的发送,通过objc_msgSend进行发送

objc_msgSend

objc_msgSend 汇编

在源码中全局查找,发现objc_msgSend在不同的系统架构中,源码不太一样,我们采用真机架构来分析objc_msgSend流程;

arm64.png

查看真机arm64的框架中,objc_msgSend的源码;

objc_msgSend.png

objc_msgSend源码非常简单,并且是采用汇编语言编写的;是不是看不懂,🤣🤣🤣而且很奇怪,为什么在这里,苹果就使用汇编语言,而不是C/C++;并且p0,x0等这些是什么含义呢?

看源码,得知objc_msgSend是汇编语言,在运行中,汇编存在寄存器结构,模拟器的寄存器结构标记为:rax、rbx、rcx、rdx等,里面保存着self,和sel等数据,如下图:

模拟器寄存器.png

在 arm64上,寄存器结构为x0~x30等;self 是保存在x0中,sel 在x1里,如下图:

arm64寄存器.png

objc_msgSend 源码分析

cmp p0,#0

cmp,对比指令;对比p0,#0;

p0的地址,消息接收者是person,意味着p0,代表的就是person的地址;

#0,对比对象,值为0

cmp p0, #0                        //对比当前消息接收者,是否存在,是否有消息接收者;不存在,则执行这个if else 逻辑
    #if SUPPORT_TAGGED_POINTERS   //接着判断当前是否为 tagged pointer (小对象)类型
    b.le LNilOrTagged             //  (MSB tagged pointer looks negative)
    #else
    b.eq LReturnZero              //person不是个小对象类型,则执行这里的代码;b.eq 相等,则返回默认值->空消息;因为没有消息接收者,所以返回空消息
    
#endif                            //消息接收者存在的话,执行下面的逻辑
ldr p13, [x0] // p13 = isa        [x0] 为对比对象
GetClassFromIsa_p16 p13, 1, x0    // p16 = class
LGetIsaDone:                      // 取值结束后,执行下面的逻辑

// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

ldr p13, [x0]

ldr,加载指令,加载p13、[x0]

p13:是isa

[x0]:消息接收者

GetClassFromIsa_p16 p13, 1, x0:对p13、x0、1等这些数据,进行处理;

GetClassFromIsa_p16 源码分析:

1.png

截屏2021-07-21 下午5.22.57.png

截屏2021-07-22 上午10.10.34.png

__has_feature:判断编译器,对()内的条件,是否支持; ptrauth_calls:针对 arm64 架构,进行指针验证;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构,具体可参考苹果原文; developer.apple.com


所以,发消息之前,需要通过消息接收者,去获取class。相当于,需要通过person对象,去获取LGPerson这个class;

获取class,主要是因为,objc_msgSend是存在cache内,而cache是保存在class中,所以,必须得到class,才能执行objc_msgSend;

CacheLookup

我们知道,objc_msgSend其实就是通过 sel 去查找imp的一个过程;那么我们就来分析一下这个过程;

上一章节,主要讲通过对比消息接收者,去获取isa,再根据isa,获取class,接下来讲解为什么要获取的class,它的用处是什么。

获取 index

在源码中,LGetIsaDone结束后,执行了CacheLookup这个函数;

cachelookup-第 2 页.png

在上篇文章中,我们知道,sel、imp是存储在bucket中,而buckets存在cache_t中,所以在上图中,通过掩码&cache_t,可以拿到buckes;

但是在arm架构下,汇编的源码,比较复杂,所以我们切换架构,采用mac架构分析接下来的流程; 毕竟现在我们是在mac系统下,源码结合汇编,能更简单直接的分析源码;

我们需要通过sel去获取方法对应的imp,而imp存在所bucket内,buckets里,包含很多个bucket,所以需要知道imp对应的bucket,获取bucket,需要知道bucket对应的下标,下标可以通过哈希获取;

cachelookup-第 3 页.png

通过上述流程,我们获取到了buckets、index等数据,得到这些数据后,接下来,我们就开始获取对应的bucket;

获取要查找bucket

cachelookup-第 4 页.png

cachelookup-第 6 页.png

查找bucket的流程,也是通过不断的内存地址平移获取,那么得到bucket后,我们需要查找sel对应的imp。那么就是执行CacheHit函数;

CacheHit

cachelookup-第 5 页.png

总结

在代码的基础上,我们分析了objc_msgSend的发消息流程。最后简单做一下总结:

通过对比消息接收者,获取isa,再通过isa获取到class对象;class 通过内存平移,得到cache;sel、imp 保存在 bucket结构内,buckets内部包含多个bucket结构; buckets 结构是cache结构的组成部分,通过掩码&cache_t获取buckets,再通过index下标,再buckets内部,获取对应的bucket;得到bucket,sel、imp就能获取出来;

快捷键小知识

搜索代码关键词结果比较多的时候,如何快捷查看;

按住command,再点击下拉箭头即可收起

快捷.png

结果如下图:

结果.png

如此,就能查看搜索的关键词所在文件。