OC 原理探索:objc_msgSend 流程

1,997 阅读7分钟

前言

OC 类原理探索:cache 结构分析补充 中我们对cache结构进行了补充,也引入到了objc_msgSend,今天主要任务是探索objc_msgSend的汇编源码。

准备工作

一、runtime 的运行时理解

Runtime 简介

  • Runtime是一个用CC++汇编编写的运行时库,包含了很多C语言的API,封装了很多动态性相关的函数。
  • Objective-C是一门动态运行时语言,允许很多操作推迟到程序运行时再进行。OC的动态性就是由Runtime来支撑和实现的,Rumtime就是它的核心。
  • 我们平时编写的OC代码,底层都是转换成了Runtime API进行调用。

编译时

编译时顾名思义就是正在编译的时候。

那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码。 (当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。)

编译时就是简单的作一些翻译工作,比如检查老兄你有没有粗心写错啥关键字了啊。有啥词法分析,语法分析之类的过程。就像个老师检查学生的作文中有没有错别字和病句一样,如果发现啥错误编译器就告诉你。

如果你用微软的VS的话,点下build。那就开始编译,如果下面有errors或者warning信息,那都是编译器检查出来的。所谓这时的错误就叫编译时错误,这个过程中所做的类型检查也就叫编译时类型检查,或静态类型检查(所谓静态嘛就是没真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。

所以有时一些人说编译时还分配内存啥的肯定是错误的说法。

运行时

运行时就是代码跑起来了,被装载到内存中去了。(你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内存中才变成活的)。

运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。

Runtime 版本

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

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

可以在官方文档 Objective-C Runtime Programming Guide 中找到相关定义。

image.png

Runtime 的发起方式

Runtime的层级结构:

image.png

Runtime的三种发起方式OC 方法NSObject 接口objc api

image.png

方法调用的本质 objc_msgSend

创建SSPerson类,say1:方法有实现,say2方法没有实现:

image.png 实例化SSPerson,调用say1:方法、say2方法,command + B进行编译,编译成功:

image.png

command + R运行项目,项目报错:

image.png

这就是编译时运行时的区别,接下来clang -rewrite-objc main.m -o main.cpp编译main.m得到main.cpp文件。

在文件中查看main函数:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        SSLPerson *person = objc_msgSend((id)objc_getClass("SSLPerson"), sel_registerName("alloc"));

        objc_msgSend((id)person, sel_registerName("say1:"), (NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_d1fdf2_mi_1);
         ((id)person, sel_registerName("say2"));
    }
    return 0;
}
  • 这里的方法调用,采用的是Runtime API调用方式;
  • 可以看到,不管是类方法还是对象方法都是用到了objc_msgSend,方法调用的本质是消息的发送
  • 调用方法 = objc_msgSend(消息的接受者,消息的主体(sel + 参数))

我们添加一个say3方法,然后用objc_msgSend进行调用:

image.png

可以正常调用,注意下面的设置要改为NO,正常是YES的。

image.png

objc_msgSendSuper

找到objc_msgSendSuper的定义:

image.png

我们看到有函数中有objc_super参数,我们去源码中看下它的定义:

image.png

  • objc_super中有receiversuper_class两个成员变量;
  • super_class是第一查找对象,如果没有方法实现的话会继续向上寻找,直到NSObject类。

我们来重新定义下类,SSLPerson继承SSLAnimalSSLAnimal去实现say2方法:

image.png

super_class赋值为SSLAnimal.class,执行代码可以正常打印:

image.png

接下来把super_class赋值为SSLPerson.class,执行代码:

image.png

同样可以正常打印,这也证明了我们上面的说法,方法寻找是会向上寻找的。

二、objc_msgSend 流程

objc_msgSend 源码查找

打断点到方法调用处,用汇编跟源码的方式找到objc_msgSend所在源码库objc

image.png

源码中搜索objc_msgSend,调用的地方会非常非常的多,很难查看:

image.png

按住command + 点击下拉箭头,收缩文件:

image.png

  • objc_msgSend的源码在汇编中,汇编文件是以.s结尾的;
  • 我们来看objc-msg-arm64.s文件,因为arm64是真机架构,i386是模拟器架构,x86_64Mac OS架构。

我们接下来通过汇编来探索objc_msgSend的流程,ENTRY是汇编程序的入口点,我们找到它:

image.png

汇编源码 解析

1. 汇编源码 消息接受者判空

汇编源码:

image.png

  • 源码解析
    • 判断消息接受者person是否为空,如果为空;
    • 判断是否为tagged pointer,如果是的话进行相关处理;
    • 如果不是tagged pointer,置空,结束方法调用; image.png
    • 如果消息接受者person不为空,向下继续执行。

2. 汇编源码 获取isa

image.png

  • 源码解析
    • personisa赋值给p13
    • p13以参数形式,传进GetClassFromIsa_p16
    • 通过ExtractISAisa & ISA_MASK赋值给p16,也就得到了class
    • 得到class,是为了获取其成员变量cache,进行方法的查找;
    • GetIsaDone:获取isa结束。

3. 汇编源码 CacheLookup

image.png

  • CacheLookup宏定义函数,Mode = NORMALFunction = _objc_msgSendMissLabelDynamic = __objc_msgSend_uncached

4. 汇编源码 获取hash index

汇编源码:

image.png

  • mov x15, x16
    • 保存原始isa值,p16 = classx15 = x16 = isa
  • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    • 真机架构,也是我们此流程分析的架构。
  • ldr p11, [x16, #CACHE]
    • #define CACHE (2 * __SIZEOF_POINTER__)
      • CACHE = 2 * __SIZEOF_POINTER__ = 16
    • p11 = isa 平移 16 = cache_t
  • CONFIG_USE_PREOPT_CACHES
    • 真机环境值为1
  • __has_feature(ptrauth_calls)
    • 判断是否为A12以后机型
  • tbnz p11, #0, LLookupPreopt\Function
    • cache_t0号位置是否为0,不为0跳转到LLookupPreopt\Function去加载共享缓存,暂时不对LLookupPreopt\Function进行探索
  • and p10, p11, #0x0000ffffffffffff
    • p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    • 0x0000ffffffffffff
      • image.png
      • 0000000000000000111111111111111111111111111111111111111111111111
  • eor p12, p1, p1, LSR #7and p12, p12, p11, LSR #48
    • 根据hash 函数获取indexp12 = (_cmd ^ (_cmd >> 7)) & mask
    • image.png

5. 汇编源码 向前遍历查找

汇编源码:

image.png

  • add p13, p10, p12, LSL #(1+PTRSHIFT)
    • PTRSHIFT的定义:#define PTRSHIFT 3
    • p13 = buckets + (index << 4)p13就是index下的bucket,是我们第一个要查的bucket
      • index << 4相当于index * 16
  • 1: ldp p17, p9, [x13], #-BUCKET_SIZE
    • p17 = impp9 = sel,然后*bucket--
  • cmp p9, p1
    • 比较sel_cmd是否不等
  • b.ne 3f
    • 如果不相等,向前查找下一个bucket,跳到3f
  • 2: CacheHit
    • 如果相等,进入CacheHit缓存命中,返回
  • 3: cbz p9, \MissLabelDynamic
    • 判断sel是否为0,如果为0进入__objc_msgSend_uncached函数
  • cmp p13, p10b.hs 1b
    • while (bucket >= buckets),如果bucket >= buckets,跳到上面1b,否则进行下面的代码

6. 汇编源码 最后位置 向前遍历查找

汇编源码:

image.png

  • add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
    • p13 = buckets + (mask << 1+PTRSHIFT),平移到buckets的最后一个位置
  • add p12, p10, p12, LSL #(1+PTRSHIFT)
  • 4: ldp p17, p9, [x13], #-BUCKET_SIZE
    • {imp, sel} = *bucket--,向前推移去取值
  • cmp p9, p1b.eq 2b
    • if (sel == _cmd),跳到2b CacheHit
  • cmp p9, #0
    • while (sel != 0 &&
  • ccmp p13, p12, #0, ne
    • bucket > first_probed)
  • b.hi 4b,回到4b继续执行

三、真机跑汇编

新创建一个项目,接下来用汇编跟源码的方式进行探索,汇编跟源码有不懂的可以去 OC 对象原理探索(一)中查看。

image.png

如下是真机调试的汇编,可以发现跟汇编源码非常相似。

image.png

我们进行一些简单的调试,和对汇编源码的一些验证。

image.png

  • 根据打印可以看到,x1确实是sel

image.png

  • 读取x0得到<SSLPerson: 0x280d78060>,确实是消息接受者person
  • 读取x16地址为0x00000001044415e0,与SLPerson.class相同,可以证明x16classx16 = x13 & 0x7ffffffffffff8也就是isa & isaMask

image.png

  • 读取x12得到0x0000000000000001也就是1,通过cache_hash函数得到的哈希 index