方法的本质2_从objc_msgSend谈起


方法的本质,就是消息传递…

本页所使用的objc runtime 756.2,来自GITHUB

一、引子:Runtime

概念

我们都知道,在运行OC代码时,类或者对象在调用方法时会用到runtime,那么,到底什么是运行时呢?

寻找一些资料后可以给出概念:

In computer science, runtime, run time or execution time is the time when the CPU is executing the machine code.
在计算机科学里,runtime,run time 或execution time 是指CPU 执行机器语言的期间。
—— 维基百科

Runtime 是一套由C、C++、汇编混合写成的为OC提供运行时功能的api。

先看苹果开发者文档里对runtime 的介绍的介绍:

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.

OC runtime 是一个给OC语言动态属性提供支持的运行时库,这些属性链接到所有的OC应用 。OC runtime库支持在shared library里实现的函数,这些函数库名为/usr/lib/libobjc.A.dylib

版本

legacy:经典版

modern:现代版,即objc2.0,我们目前用到的版本。

二、方法的本质

概念

方法的本质,就是objc_msgSend 的消息传递。先看苹果开发者文档里对objc_msgSend的介绍:

Function

objc_msgSend

Sends a message with a simple return value to an instance of a class.

发送一个有简单返回值的消息给类的实例

相关源码如下:

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

其中两个关键参数selfop 的解释如下:

* @param self A pointer to the instance of the class that is to receive the message.

​ self 一个指向由类生产的实例的指针,用来接收消息

* @param op The selector of the method that handles the message.

​ op 方法: 处理消息的方法的选择器

可见,objc_msgSend 的核心信息,就是向对象主体(self)传递相应的方法/消息(op)

但是消息传递的机制到底怎样,还是用源码来解释。

源码分析

开始生成一个main.m 内代码如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];

        // Setup code that might create autoreleased objects goes here.
    }
    return NSApplicationMain(argc, argv);
}

其中LGPerson 的内部实现如下:

@interface LGPerson : NSObject

- (void)sayHello;
- (void)sayNB;

@end

在这里,着重查看LGPerson alloc 方法,以及其实例 person sayHello` 在汇编里的实现 :

进入到目录下,输入编译代码:

clang -rewrite-objc main.m

得到main.cpp 的文件

编译c++ 指令

cpp 生成后的结构图

打开main.cpp 结构如下:

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    }
    return NSApplicationMain(argc, argv);
}

上文代码中,可以简化为,

Runtime 语法 OC 语法
(LGPerson ()(id, SEL))(void *)
(id)objc_getClass(“LGPerson”) [LGPerson class]
sel_registerName(“alloc”) @selector(alloc)

即代码为:

LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));

// 转换后
LGPerson *person = objc_msgSend([LGPerson class], @selector(alloc));

解释为: 向LGPerson 的类,发送了alloc 方法

扩展代码实践

开始设定一个类Student

#import "Student.h"
interface Student : Person
- (void)sayCode;
@end
OC语法
#import "Student.h"

Student *student = [Student new];
[student sayCode];
NSObject 写法
objc_msgSend(student, NSSelectorFromString(@"sayCode"));
sel_registerName 的函数API
objc_msgSend(student, sel_registerName("sayCode"));

三、底层分析

汇编源码

我们现在分析,当类对象发送消息是,底层发生了什么。

1、方法入口

新建工程,输入如下代码,进行断点检测;

另外在Xcode 的Debug–Debug Workflow—Always show Disassembly

配置:打开汇编监视

OC方法调用,打开断点

按住Control 逐渐获得汇编函数得到第一个方法传递函数:objc_msgSend

很明显,这里的Student sayCode 在汇编里,执行了 objc_msgSend 方法,继续查看

打开Xcode, 搜索objc_msgSend,找到相关结果如下:

objc_msgSend 搜索结果

由于研究的环境是移动平台,选择arm64,通过ENTRY _objc_msgSend结果进入

首先看到的代码如下:

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

1    cmp    p0, #0            // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
2    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
3    b.eq    LReturnZero
#endif
2、类Tagged Pointer 检查

代码解析:

序号 代码 解释
1 cmp p0 #0 cmp = compare
检测0位寄存器 = 空?,以及tagged point 检测
2 b.le LNilOrTagged // 即1代码成立,跳转至LNilOrTagged的宏(下文叙述)
3 b.eq LReturnZero // b.eq 即不成立,结果为空,返回并跳出

这一小节,主要是用来判断 tagged pointer 是否存在,存在则继续进行,否则跳出。

Tagged point是苹果推出的针对64位机器的特定的指针,概念如下:

苹果对于Tagged Pointer特点的介绍:

  1. Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。
3、加载isa

这一部分主要是通过加载的isa,获取当前底层的类的实现

  // person - isa - 类
    ldr    p13, [x0]                            // p13 = isa
    GetClassFromIsa_p16 p13        // p16 = class
序号 代码 解释
1 ldr p13, [x0] 将13位存储器isa 字,加载到0位寄存器
LDR = LoaD woRd
2 GetClassFromIsa_p16 p13 通过加载的isa,宏逻辑获取到当前的类
GetClassFromIsa_p16 是一个汇编宏

GetClassFromIsa_p16 的汇编实现如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov    p16, $0            // optimistically set dst = src
    tbz    p16, #ISA_INDEX_IS_NPI_BIT, 1f    // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add    x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr    p16, [x10, p16, UXTP #PTRSHIFT]    // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and    p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov    p16, $0

#endif

.endmacro

分析流程图如下:

GetClassFromIsa_p16 流程图

4、用isa查询方法缓存

当在3步,isa 拿到之后,现在要做的事情,就是对当前要执行的方法进行缓存查找。

LGetIsaDone:
    CacheLookup NORMAL        // calls imp or objc_msgSend_uncached

CacheLookup 在这里的做法是查询类里是否含有方法到缓存。 一般有两种结果:拿到缓存IMP,或者未曾缓存。

查询可以得到有三种查询方式:

  • NORMAL 正常查找
  • GETIMP 获取IMP
  • LOOKUP 慢速查询方法

根据源码,做了一些注释:

CacheLookup宏的流程

其中多次出现CheckMiss ,也是个汇编宏,使用在缓存查找失败后。

源码如下:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz    p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz    p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz    p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

根据查找的模式位NORMAL, 对应的*__objc_msgSend_uncached *

在源码中搜索,得到相关逻辑如下:

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search

    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

执行方法查找的核心方法,就是MethodTableLookup, 继续点开查看,得到的是方法列表查找

这里的内容,则是到objc_classclass_data_bits_t 里寻找方法的具体实现了,下一篇文章我们来讲。

四、小结

这一篇,主要是开始从汇编的角度,来实现方法查找流程,流程草写了一下,图一定补。。。

  • 拿到isa
  • 查找Class
  • 在Cache_t 查找bucket
    • bucket 相同,返回IMP
    • 否则 跳到BITS
  • BITS 中
    • 查找Rw
      • 查找ro
        • 查找methodList

总流程如下:

objc_msgSend


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
方法的本质3_消息查找流程 方法的本质3_消息查找流程
本页所使用的objc runtime 756.2,来自GITHUB 1. 概念在前文中,已经总结了方法查找的流程,今天从代码层面上继续阐述。 isa 的指向图如下所示: 2. 方法查找流程2.1 从业务代码分析配置代码环境:,先
2020-03-06 李佳
下一篇 
方法的本质1--cache_t方法缓存分析 方法的本质1--cache_t方法缓存分析
本页所使用的objc runtime 756.2,来自GITHUB 1.概念1.1 objc_class 结构前面探索了类的结构,知道了类的结构本质上是objc_class的结构体,而在 C 源码例, objc_class 结构体的结构
2020-01-21 李佳
  目录