OC底层原理十二: objc_msgSend(方法快速查找)

OC底层原理 学习大纲

上一节已了解类的cache结构插入操作。但是有几个问题:

  • 1. 何时插入缓存?
  • 2. 缓存读取机制是怎样?
cache分析流程

现在开始探索之旅

1. 探索插入操作
2. 介绍Runtime
3. 了解方法的本质
4. objc_msgSend解析

1. 探索插入操作

我们从insert开始寻找谁在调用它

  • 在objc4源码下搜索->insert(c++的调用方式->
OC底层原理十二: objc_msgSend(方法快速查找)_第1张图片
image.png
  • 发现cache_fill调用了它,我们继续搜索cache_fill:
OC底层原理十二: objc_msgSend(方法快速查找)_第2张图片
image.png

我们发现,在缓存写入之前,我们需要先知道:

  • 给谁进行写入objc_msgSend
  • 写入内容是什么cache_getImp

到这里,我们必须引出OC非常重要的机制:Runtime运行时

2. 介绍Runtime

Objective-C Runtime Programming Guide,此文档不再更新,适合初步了解Runtime

2.1 什么是Runtime

  • Runtime是一个由CC++汇编混合开发的API库,它将程序的一些决定性工作编译器推迟到运行期,使得OC语言具备动态特性。内部使用消息机制进行通信

2.2 什么是运行时? 什么是编译时?

  • 编译时:编译器将源代码翻译成机器识别代码。这是一个静态操作,并不会把代码写入内存中进行运行
  • 编译过程中,会分析语法是否正确。
  • 编译时提示的errorwarning都是编译时错误
  • 编译过程检查就叫编译时类型检查静态类型检查
  • 运行时: 将代码装载内存中,让代码运行起来
  • 代码在装载内存之前,只是个"死家伙",静静地趴在磁盘中。只有载入内存,才是"活的"
  • 运行时类型检查与前面所说的编译时类型检查(或叫静态类型检查)不一样,它不是简单的扫描代码,而是在内存中做些操作,做些判断。(是动态活动的)

例如一个函数,只声明未实现command+B编译时不会报错,但是command+R运行时会报错。

image.png

2.3 Runtime版本

  • Runtime有两个版本, Legacy(早期版本) 和Modern(现行版本)
  • 早期版本: Objective-C 1.0,用于32位Mac OS X的平台上,实例变量发生改变后,需要重新编译其子类
  • 现行版本: Objective-C 2.0,用于iPhone程序和Mac OS X v10.5以后的系统中的 64 位程序实例变量发生改变后,不需要重新编译其子类

2.4 运行时让OC具备多态特性

  • OC的运行时机制:将数据类型的确定由编译时,推迟到运行时。OC的这种运行时机制使对象的类型对象的属性方法运行时才能确定

  • 多态: 不同对象以自己的方式响应相同的消息的能力叫做多态

例如:
自然界中的人类(Person)都有一个相同的方法-sing,男人(Man)类属于人类,女人(Wonan)类也属于人类,都继承了人类后,会实现各自的-sing方法。但是自然界中男人和女人的sing风格又不一样,男人唱的豪迈女人唱的委婉,但都继承了person唱的能力,这就是多态的现象。
也就是不同的对象以自己的方式响应相同消息的能力叫多态。 也可以说运行时机制是多态基础
描述参考: 码上江湖

2.5 Runtime的三种调用

  • oc代码调用、framework调用 、RuntimeAPI调用


    OC底层原理十二: objc_msgSend(方法快速查找)_第3张图片
    image.png

2.6 探索runtime

先准备好静态资源的,咱们才能动态起来。所以先获取compiler层文件。

  • 测试代码:
@interface HTPerson : NSObject
- (void)sayHello;
@end

@implementation HTPerson
- (void)sayHello{
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        HTPerson *p  = [HTPerson alloc];
        [p sayHello];
    }
    return 0;
}
  • clang静态编译main.m文件: clang -rewrite-objc main.m -o main.cpp
OC底层原理十二: objc_msgSend(方法快速查找)_第4张图片
image.png

我们发现,所有OC方法,不管是类方法alloc,还是实例方法sayHello都是调用了objc_msgSend发送消息。

  • 调用格式: objc_msgSend: (消息接收者, 消息主体)

  • 尝试手动使用objc_msgSend执行方法:

  1. 导入头文件#import
  2. 手动关闭运行时的编译警告: buildSetiing->Enable Strict Checking of objc_msgSend Calls->设置为No
  3. 加入测试代码objc_msgSend(p, sel_registerName("sayHello"));
  4. 打印查看:
OC底层原理十二: objc_msgSend(方法快速查找)_第5张图片
image.png

崩溃暂时不理会,我们现在发现sayHello打印成功了

3. 了解方法的本质

我们知道,方法的本质就是一个方法名和对应的函数代码

  • OC中,我们使用执行对象+函数名进行函数调用 (例如:[person sayHello])。

  • 内部完整的调用流程是: objc_msgSend发送消息(class sel) -> 通过sel(方法编号)找到imp(函数指针地址) -> 找到函数内容

  • 第一步: 发送消息我们有三种API调用方法(oc代码调用、framework调用 、RuntimeAPI调用)
  • 第三步:可直接 从函数指针地址读取函数内容

上述流程,我们唯一不知道的就是系统如何通过sel(方法编号)找到imp(函数指针地址)?

4. objc_msgSend解析

了解sel如何找到imp就是探究 objc_msgSend内部机制。

  • 函数的调用是极其频繁的,所以对性能的要求非常高。objc_msgSend使用汇编进行编写

  • imp的查找分为2个阶段,快速查找(缓存cache中,汇编编写)和慢速查找(方法列表methodTable中,c和c++编写),今天先介绍快速查找

接下来的知识,需要大家先熟悉cache_t的结构

汇编查找函数的流程: 从指定类开始->定位cache->定位buckets->哈希运算获取首次位置->循环寻找位置->返回impnull

  • 打开objc4源码,搜索objc_msgsend,我们选择arm64真机环境进行探索(其他环境也是类似逻辑)。

  • 找到objc_msgSend入口

OC底层原理十二: objc_msgSend(方法快速查找)_第6张图片
image.png
  • 初始化数据, 从receiver中读取isa中的

    OC底层原理十二: objc_msgSend(方法快速查找)_第7张图片
    image.png

  • 当前objc-msg-arm64.s汇编文件中搜索CacheLookup,找到.macro CacheLookup定义处:

OC底层原理十二: objc_msgSend(方法快速查找)_第8张图片
image.png
  • 如果匹配sel成功,调用Cachehit命中缓存流程, 返回找到的imp
OC底层原理十二: objc_msgSend(方法快速查找)_第9张图片
image.png
  • 如果匹配失败,触发CheckMissJumpMiss流程, 告知外部Cache未找到imp

    OC底层原理十二: objc_msgSend(方法快速查找)_第10张图片
    image.png

  • 未找到imp时,进入__objc_msgSend_uncached流程,搜索__objc_msgSend_uncached

OC底层原理十二: objc_msgSend(方法快速查找)_第11张图片
image.png
  • 发现缓存找不到后,进入方法列表去查找。 搜索MethodTableLookup
OC底层原理十二: objc_msgSend(方法快速查找)_第12张图片
image.png
  • 跳转_lookUpImpOrForward,进入慢速查找阶段。

奉上完整流程

OC底层原理十二: objc_msgSend(方法快速查找)_第13张图片
image.png

下一节: OC底层原理十三: objc_msgSend(方法慢速查找)

你可能感兴趣的:(OC底层原理十二: objc_msgSend(方法快速查找))