Date Notes Refers.
2019-07-20 首次提交 objc4-750.1
2019-08-24 整理结构,未完待续 -

0

Preface

Obj-C 中方法调用的本质是消息发送机制,即 [foo bar] 是向 foo 对象发送一条 bar 的消息,而消息发送就是通过 objc_msgSend 所进行的。那么这次本文就简单窥探一下 objc_msgSend 吧。

Why

在开始之前,先思考以下为什么 Obj-C 中方法调用的本质是 objc_msgSend 呢?

我们创建一个使用 Obj-C 的 iOS 项目,如下在 ViewController 中添加一个按钮,并在按钮的点击事件中创建一个 Obj-C 对象,再调用其方法:

#import "ViewController.h"

@interface Foo : NSObject
- (void)bar;
@end

@implementation Foo
- (void)bar {}
@end

@interface ViewController ()
@end

@implementation ViewController

- (IBAction)clickOnButton:(UIButton *)sender {
    Foo *foo = [[Foo alloc] init];

    [foo bar]; // 🔴 Breakpoint
}

@end

我们将断点打在 [foo bar]; 一行,启动程序并点击按钮。在 Xcode 的控制台多次输入 si(Step Into)即可最终跳转到 objc_msgSend

3

或者我们也可以使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o foo.cpp 将 Obj-C 代码翻译为 C/C++:

static void _I_ViewController_clickOnButton_(ViewController * self, SEL _cmd, UIButton *sender) {
    Foo *foo = ((Foo *(*)(id, SEL))(void *)objc_msgSend)((id)((Foo *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Foo"), sel_registerName("alloc")), sel_registerName("init"));

    // objc_msgSend(foo, sel_registerName("bar"))
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)foo, sel_registerName("bar"));
}

综上,我们可以说 Obj-C 中方法调用的本质即是 objc_msgSend

Steps

objc_msgSend 总共分为消息发送、动态方法解析、以及消息转发三大部分,下面我们就依次来研究一下。

消息发送

objc_msgSend 中的第一个部分是消息发送,即对消息接收者发送一条方法消息,当接收者可以处理消息时将执行相应的方法,无法处理时则进入下一步骤。

Where & Why

在 Apple 开源的 objc4 源码中,我们似乎只能在「message.h」中找到 objc_msgSend 的声明,其将消息接收者(即对象)作为第一个参数,将消息(即方法选择器)作为第二个参数,并将方法的参数追加在参数列表的最后:

// message.h

/**
 * Sends a message with a simple return value to an instance of a class.
 * 发送一个带有简易返回值的消息到一个类的实例。
 *
 * @param self A pointer to the instance of the class that is to receive the message.
 *             指向接收消息者实例的指针。
 * @param op The selector of the method that handles the message.
 *           处理消息的选择器。
 * @param ...
 *   A variable argument list containing the arguments to the method.
 *   包含方法参数的可变参数列表。
 *
 * @return The return value of the method.
 *         方法的返回值。
 *
 * @note When it encounters a method call, the compiler generates a call to one of the
 *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
 *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
 *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
 *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
 * 注意:当遇到方法调用时,编译器会生成对 objc_msgSend、objc_msgSend_stret、objc_msgSendSuper、objc_msgSendSuper_stret 四个函数之一的调用。
 * 到达对象父类(使用 super 关键字)的消息通过 objc_msgSendSuper 发送;其它消息则通过 objc_msgSend 发送。
 * 返回值为结构体的消息通过 objc_msgSendSuper_stret 或 objc_msgSend_stret 发送。
 *
 */
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

1

objc_msgSend 的具体实现是由汇编语言编写的,原因有两点:

  1. 对性能的极致追求,每一条汇编指令都对应一条机器指令,使用汇编便于针对不同架构的 CPU 优化每一条指令的速度;
  2. C 语言无法实现一个保存未知参数且支持跳转到任一函数指针处的函数(引自 Dissecting objc_msgSend on ARM64 - Mike Ash)。

由于 objc_msgSend 整个流程比较复杂,我将其分为多个用例,逐个分析。

当接收者为 nil

当接收者为 nil 时,即 [foo bar] 中的 foonil

- (IBAction)clickOnButton:(UIButton *)sender {
    Foo *foo = [[Foo alloc] init];

    // 将接收者置为 nil
    foo = nil;
    [foo bar];
}

// LLDB:
// 进入 objc_msgSend 后可以尝试使用 LLDB 命令读取 x0 寄存器中存储的值
// (lldb) register read x0
//       x0 = 0x0000000000000000

4

在 Xcode 中 si 执行即可看到具体的汇编代码跳转,也与上图的源码分析一致:

libobjc.A.dylib`objc_msgSend:
    0x192bd8180 <+0>:   cmp    x0, #0x0                  ; =0x0
    0x192bd8184 <+4>:   b.le   0x192bd81f8               ; <+120>
    ; ...
    0x192bd81f8 <+120>: b.eq   0x192bd8230               ; <+176>
    ; ...
    0x192bd8230 <+176>: mov    x1, #0x0
    0x192bd8234 <+180>: movi   d0, #0000000000000000
    0x192bd8238 <+184>: movi   d1, #0000000000000000
    0x192bd823c <+188>: movi   d2, #0000000000000000
    0x192bd8240 <+192>: movi   d3, #0000000000000000
    0x192bd8244 <+196>: ret

当未命中缓存时

5

当命中缓存时

SUPPORT_INDEXED_ISA

SUPPORT_INDEXED_ISA,即是否支持索引化 isa 我们可以在源码中找到这段定义的宏:

// objc-config.h

// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa
// field as an index into a class table.
// 在将类存储在 isa 域中并作为类表索引的平台上定义 SUPPORT_INDEXED_ISA=1。
// Note, keep this in sync with any .s files which also define it.
// Be sure to edit objc-abi.h as well.
#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

比较简单的验证方式是我们可以直接在指定真机运行的代码中尝试获取最终的值:

#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

// Use of undeclared identifier '__ARM_ARCH_7K__'
// NSLog(@"%d", __ARM_ARCH_7K__);
NSLog(@"%d", __arm64__);           // 1
NSLog(@"%d", __LP64__);            // 1
NSLog(@"%d", SUPPORT_INDEXED_ISA); // 0

当然,我还是要细究一下。

__ARM_ARCH_7K__ 根据名称可以得出是定义在目标为 ARM 7k 架构 CPU 的代码中的标志宏。我们可以在 LLVM 开源的「ARM.cpp」找到其定义:

// ARM.cpp

// Unfortunately, __ARM_ARCH_7K__ is now more of an ABI descriptor. The CPU
// happens to be Cortex-A7 though, so it should still get __ARM_ARCH_7A__.
if (getTriple().isWatchABI())
  Builder.defineMacro("__ARM_ARCH_7K__", "2");

__arm64__ 即是定义在目标为 ARM 64 架构 CPU 的代码中;__LP64__ 则意味着 Long Pointer,该标志宏为 1 的代码中 long int 和指针类型(指针中存储的是内存地址,也即内存地址)的长度为 64 位(8 字节),int 为 32 位(4 字节)。

综上,在 iOS 的真机设备中,SUPPORT_INDEXED_ISA 的值最终为 0


在理解上面汇编代码的同时,需要了解 Obj-C 中对象内部的 cache_t 即缓存的内部结构:

// objc-runtime-new.h
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};
  • C 语言使用静态绑定(Static Binding)来进行函数调用,在编译时刻就能决定运行时所应调用的函数,即在不考虑内联下,会直接生成调用函数的指令,函数地址则被硬编码在指令中。
  • C 语言中使用函数指针进行调用则属于动态绑定(Dynamic Binding),所要调用的函数在运行时才能确定。

    // objc_msgSend 的原型:
    // id self:接收者本身
    // SEL cmd:方法选择器本身
    // ...:cmd 方法的参数(可变参数函数)
    void objc_msgSend(id self, SEL cmd, ...)
    
  • Obj-C 中的方法使用动态绑定调用。

  • Obj-C 对象方法查找顺序:本类的类对象方法列表中查找同名方法(分类 Category 中的方法会合并到类的方法列表中,根据后编译更靠前,会被先找到) -> 若没有找到,则根据 superclass 指针向父类的类对象方法列表中查找,直到查找基类 NSObject 的类对象中也没有则进行消息转发(Message Forwarding)。

  • objc_msgSend 会将方法的匹配的结果缓存在类对象的 cache_t cache; 中,优化之后查找相同方法的速度,但仍慢于静态绑定,不过也不至于是瓶颈;如果对查找速度极致追求则可使用纯 C 函数。

  • 边界情况:

    • objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于 CPU 寄存器中(比如返回的结构体太大了),那么就交由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
    • objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理。在某些结构的 CPU 中调用函数时,需要对「浮点数寄存器(Floating-Point Register)」做特殊处理,也就是说,通常所用的 objc_msgSend 在这种情况下并不合适。这个函数是为了处理 x86 等架构 CPU 中某些令人稍觉惊讶的奇怪状况。(不会使用在 ARM 64 架构中)
    • objc_msgSendSuper:如果要给父类发送消息,例如 [super message:parameter],那么就交由此函数处理。也有另外两个与 objc_msgSend_stretobjc_msgSend_fpret 等效的函数,用于处理发给 super 的相应的消息。
  • Obj-C 对象的每个方法都可以视为简单的 C 函数,其原型为:<return_type> Class_selector(id self, SEL _cmd, ...)

  • Obj-C 每个类中都有以上原型函数表,objc_msgSend 等函数通过该表寻找应该执行的方法并跳至其实现(原型与 objc_msgSend 函数类似是为了利用尾调用优化 Tail-Call Optimization 技术,简化跳至实现的操作)。

  • 如果某个函数的最后一项操作是调用另外一个函数,那么就可以运用「尾调用优化」技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的「栈帧 Frame Stack」。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行「尾调用优化」。如果不这样的话,每次调用 Obj-C 方法之前都需要需要为 objc_msgSend 准备栈帧(可以在栈追踪 Stack Trace)中看到这种栈帧)。此外,若是不优化,还会过早地发生「栈溢出(Stack Overflow)」现象。

Reference

  • Dissecting objc_msgSend on ARM64 - Mike Ash
  • ARM Software development tools - arm.com
  • 1b and 1f in GNU assembly - StackOverflow
  • ARM.cpp - clang
  • 预定义宏 - IBM

    0x192bd8180 <+0>:   cmp    x0, #0x0                  ; =0x0
    0x192bd8184 <+4>:   b.le   0x192bd81f8               ; <+120>
    0x192bd8188 <+8>:   ldr    x13, [x0]
    0x192bd818c <+12>:  and    x16, x13, #0xffffffff8
    ->  0x192bd8190 <+16>:  ldr    x11, [x16, #0x10]
    0x192bd8194 <+20>:  and    x10, x11, #0xffffffffffff
    0x192bd8198 <+24>:  and    x12, x1, x11, lsr #48
    0x192bd819c <+28>:  add    x12, x10, x12, lsl #4
    0x192bd81a0 <+32>:  ldp    x17, x9, [x12]
    0x192bd81a4 <+36>:  cmp    x9, x1
    0x192bd81a8 <+40>:  b.ne   0x192bd81b4               ; <+52>
    0x192bd81ac <+44>:  eor    x17, x17, x16
    0x192bd81b0 <+48>:  br     x17
    0x192bd81b4 <+52>:  cbz    x9, 0x192bd84c0           ; _objc_msgSend_uncached
    0x192bd81b8 <+56>:  cmp    x12, x10
    0x192bd81bc <+60>:  b.eq   0x192bd81c8               ; <+72>
    0x192bd81c0 <+64>:  ldp    x17, x9, [x12, #-0x10]!
    0x192bd81c4 <+68>:  b      0x192bd81a4               ; <+36>
    0x192bd81c8 <+72>:  add    x12, x12, x11, lsr #44
    0x192bd81cc <+76>:  ldp    x17, x9, [x12]
    0x192bd81d0 <+80>:  cmp    x9, x1
    0x192bd81d4 <+84>:  b.ne   0x192bd81e0               ; <+96>
    0x192bd81d8 <+88>:  eor    x17, x17, x16
    0x192bd81dc <+92>:  br     x17
    0x192bd81e0 <+96>:  cbz    x9, 0x192bd84c0           ; _objc_msgSend_uncached
    0x192bd81e4 <+100>: cmp    x12, x10
    0x192bd81e8 <+104>: b.eq   0x192bd81f4               ; <+116>
    0x192bd81ec <+108>: ldp    x17, x9, [x12, #-0x10]!
    0x192bd81f0 <+112>: b      0x192bd81d0               ; <+80>
    0x192bd81f4 <+116>: b      0x192bd84c0               ; _objc_msgSend_uncached
    0x192bd81f8 <+120>: b.eq   0x192bd8230               ; <+176>
    0x192bd81fc <+124>: adrp   x10, 291010
    0x192bd8200 <+128>: add    x10, x10, #0xf00          ; =0xf00
    0x192bd8204 <+132>: lsr    x11, x0, #60
    0x192bd8208 <+136>: ldr    x16, [x10, x11, lsl #3]
    0x192bd820c <+140>: adrp   x10, 291010
    0x192bd8210 <+144>: add    x10, x10, #0xe68          ; =0xe68
    0x192bd8214 <+148>: cmp    x10, x16
    0x192bd8218 <+152>: b.ne   0x192bd8190               ; <+16>
    0x192bd821c <+156>: adrp   x10, 291010
    0x192bd8220 <+160>: add    x10, x10, #0xf80          ; =0xf80
    0x192bd8224 <+164>: ubfx   x11, x0, #52, #8
    0x192bd8228 <+168>: ldr    x16, [x10, x11, lsl #3]
    0x192bd822c <+172>: b      0x192bd8190               ; <+16>
    0x192bd8230 <+176>: mov    x1, #0x0
    0x192bd8234 <+180>: movi   d0, #0000000000000000
    0x192bd8238 <+184>: movi   d1, #0000000000000000
    0x192bd823c <+188>: movi   d2, #0000000000000000
    0x192bd8240 <+192>: movi   d3, #0000000000000000
    0x192bd8244 <+196>: ret
    0x192bd8248 <+200>: nop
    0x192bd824c <+204>: nop
    0x192bd8250 <+208>: nop
    0x192bd8254 <+212>: nop
    0x192bd8258 <+216>: nop
    0x192bd825c <+220>: nop