iOS底层原理–RunTime之objc_msgSend探究(快速查找)

前言

iOS底层原理–objc_class 中的cache探究中,我们探索cache_t的时候,发现对象在调用实例方法时,在insert之前其实会调用objc_msgSend方法,如果看过这篇文章,肯定会记得这个insert方法的流程图。

但是当时我们的研究重点是cache_t,所以我们就跳过了这重要的一步。今天在本篇文章,我们就开始正式研究objc_msgSend方法。
image.png
在开始之前,我们需要了解两个重要的概念:编译时运行时

编译时和运行时介绍

  • 编译时

顾名思义就是编译的时候。编译也就是指编译器将源代码翻译成机器可识别的代码。

编译时其实就是做一些简单的类型检查(静态类型检查),如果有error或者warning信息,这些都是编译器检查出来的。而之所以又叫谓静态类型检查是因为没有真正把代码放内存中运⾏起来,⽽只是把代码当作⽂本来扫描了一下。所以说编译时就会分配内存空间,肯定是错误的

  • 运行时(RunTime)

顾名思义就是代码运行起来的时候,被装载到内存中去了。⽽运⾏时类型检查就与前⾯讲的编译时类型检查(静态类型检查)不⼀样,不是简单的扫描代码,⽽是在内存中开始做判断,做操作等等。

  • RunTime版本
    • Legacy版本(早期版本):对应的编程接⼝:Objective-C 1.0,适用于32位的Mac OS X的平台上。
    • Modern版本(现⾏版本):对应的编程接⼝:Objective-C 2.0,iPhone程序和Mac OS X v10.5及以后的系统中的64位程序

调起RunTime的3种方式

我们调用RunTime的常见的有3种方式。

  • 通过OC代码调用,例如:[Person SayHelloWord]
  • 通过NSObject方法调用,例如:isKindOfClass
  • 通过runtime的API调用,例如:class_getInstanceSize

image.png
我们?‍?‍的代码都在Objective-C Code层。Complier(编译器层)也就是我们的编译器(LLVM)。Runtime System Library为底层相关的库,通过Complier(编译器层)在中间进行拦截转发,向Framework&RuntimeAPI提供支持.

objc_msgSend底层探究

方法本质

我们先创建一个YSHPerson类,然后定义个实例方法sayHelloWord,然后在main.m中调用该方法。接着使用clang编译main.m成main.cpp文件,看下底层是怎么实现方法的调用的。
image.png

补充:Clang语法

  1. 将 main.m 编译成 main.cpp 【clang -rewrite-objc main.m -o main.cpp

  2. 将 ViewController.m 编译成 ViewController.cpp

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun

  1. 模拟器文件编译【- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

  2. 真机文件编译【- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp

通过分析底层代码实现,我们发现其实就是调用了objc_msgSend方法,所以方法的本质也可以认为是消息发送

objc_msgSend(void /* id self, SEL op, … */ )

为了验证这种猜想,我们可以直接调用objc_msgSend方法,看下和OC直接调用方法效果是一致。

如果想直接调用msgSend方法,我们需要注意以下两个地方:

1.需要引入<objc/message.h>头文件

2.在build settings 里将enable strict checking of obc_msgSend calls由YES改成NO,将严格的检查机制关掉,否则objc_msgSend的参数会报错。

image.png
我们看下打印结果:
image.png
打印结果是一模模一样样的,证明了我们的猜想。

调用实例方法,执行父类的实现

我们再创建一个YSHStudent类,继承自YSHPerson。声明一个sayHelloWord方法,但是并不实现,我们调用该方法看下底层是如何实现的?
image.png
我们通过打印发现结果是一样的,都是调用YSHPerson的实现方法。
image.png
我们分析下objc_msgSend方法:

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, …)

发现其中有两个参数:

  • 结构体类型的objc_super
struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};
复制代码
  • SEL

至此我们发现不论是[student sayHelloWord]还是objc_msgSendSuper都执行的是父类YSHPerson中sayHelloWord的实现。我们是不是可以大胆猜测一下方法的调用过程:首先是在自身类中查找,如果自身类中没有找到,则会到父类中查找。
老规矩下面?带着我们的猜测,开始探索objc_msgSend的源码实现。

友情提示:objc_msgSend的源码不再是C++/C语言实现,而是汇编实现,不懂汇编可能会很崩溃。

objc_msgSend快速查找流程源码分析

首先我们先找到_objc_msgSend的源码,根据源码,一步一步看。
image.png

  1. cmp p0, #0 p0可以理解为x0寄存器,也就是方法的第一个参数,即p0=receiver。这里比较消息接收者和0,第一步的判断主要是用来判断是否有消息接收者,如果没有消息接收者,那么该方法的调用没有任何意义。
  2. #if SUPPORT_TAGGED_POINTERS 当是linux或者MacOS X 系统时为1,执行b.le LNilOrTagged,这句的意思如果p0<=#0,执行LNilOrTagged,我们发现这里如果p0=#0,会直接returnZero;如果不是SUPPORT_TAGGED_POINTERS,则会执行b.eq LReturnZero,直接returnZero,结束本次消息发送。
  3. 如果p0存在,也就是存在receiver,则执行ldr p13, [x0],将x0寄存器的值存入到p13中。x0存的是对象的地址,也就是isa,即p13=isa。
  4. 执行GetClassFromIsa_p16 p13, 1, x0,从字面意思理解,从isa中获取class。

image.png

  • SUPPORT_INDEXED_ISA是指是arm64架构但不是64位,这里也可以理解成32位isa指针。所以咱们直接研究__LP64__条件下的。
  • 咱们传的值needs_auth=1,所以此处执行ExtractISA p16, \src, \auth_address,src=p13(isa)。根据源码,我们得出将isaISA_MASK进行操作,根据之前咱们iOS底层原理–isa&类结构探究可以得出,这步操作其实就是得到了当前对象的类(Class),将class给p16寄存器。
.macro ExtractISA
	and    $0, $1, #ISA_MASK //$0=p16,$1=p13(isa)
.endmacro
复制代码
  1. 继续执行LGetIsaDone:CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
  • mov x15, x16,将x16(class)给x15寄存器
  • 咱们主要的研究对象是arm64架构,所以此处咱们之间看CACHE_MASK_STORAGE==CACHE_MASK_STORAGE_HIGH_16的汇编代码。等于将class向下平移16字节,也就是cache_t的位置,将cache存储到p11寄存器中,即p11=cache
#define CACHE (2 * __SIZEOF_POINTER__)  //2*8=16自己
ldr	p11, [x16, #CACHE]  //  x16 + #16 
复制代码
  • 我们看到CONFIG_USE_PREOPT_CACHES其实就是真机,正好是我们研究的。__has_feature(ptrauth_calls)是指A12芯片及已上,不是普遍模式,这里我们不多做研究。我们直接研究else分支里的汇编代码。
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
复制代码
  • 执行and p10, p11, #0x0000fffffffffffe
    tbnz p11, #0, LLookupPreopt\Function
    • p11是cache的地址,将cache的地址和0x0000fffffffffffe进行操作,得到buckets,将buckets给到p10寄存器。
    • tbnz p11的第0位如果不为0(p11[0]!=0),直接执行LLookupPreopt方法。
  • 继续往下执行eor p12, p1, p1, LSR #7
    and p12, p12, p11, LSR #48
    • p1是第二个参数SEL _cmd,eor按位异或LSR逻辑按位右移
    • p12= _cmd >> 7 ^ _cmd
    • p11 是cache_t,也就是_bucketsAndMaybeMask,将bucketsAndMaybeMask右移48位,得到mask.然后与上p12,给到p12。p12=(_cmd >> 7 ^ _cmd)&mask
  • 继续往下执行add p13, p10, p12, LSL #(1+PTRSHIFT)
    • PTRSHIFT==3,p13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask)<<4,其实p13也就是当前找到的bucket
  • 继续往下执行ldp p17, p9, [x13], #-BUCKET_SIZE
    • 将当前bucket平移一个bucket的长度,相当于*bucket–
    • bucket里面存储的是imp和sel,即p17=imp,p9=sel
  • 继续往下执行cmp p9, p1
    • 比较传进来的sel和当前拿到的bucket里的sel,如果不相等,跳转到第3处指令执行,如果相等则是,缓存命中,执行第2条指定
    • 表示不相等时直接向后跳转到局部标签1处(b: backward, f: forward)
    • cbz p9, \MissLabelDynamic,比较p9是否为0,如果为0,则执行MissLabelDynamic,MissLabelDynamic=__objc_msgSend_uncached,也就是说如果sel不存在,则执行__objc_msgSend_uncached方法。
      • p9不为0,执行cmp p13, p10,比较p13和p10,如果p13>=p10,跳转到第一条指令执行
  • 如果缓存命中,则执行CacheHit,查找sel对应的imp,然后返回IMP。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享