消息发送机制-快速查找

ios.jpg

我们知道了cache是用于方法的缓存, 并分析了cache插入sel/imp的流程.

在消息发送objc_msgSend流程中, 会先通过cache_getImp()在cache中查找方法, 找到了就走调用流程, 如果没找到继续走后续查找,转发流程.

我们知道苹果的操作系统是基于Runtime实现的, 当然objc_msgSend也是, 下面让我们先了解一下Runtime.

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.
This document looks at the NSObject class and how Objective-C programs interact with the runtime system. In particular, it examines the paradigms for dynamically loading new classes at runtime, and forwarding messages to other objects. It also provides information about how you can find information about objects while your program is running.
You should read this document to gain an understanding of how the Objective-C runtime system works and how you can take advantage of it. Typically, though, there should be little reason for you to need to know and understand this material to write a Cocoa application.

网页翻译:

Objective-C语言尽可能多地推迟从编译时间和链接时间到运行时的决定。只要有可能,它就会动态地做事。这意味着该语言不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。运行时系统是Objective-C语言的一种操作系统;它使该语言工作。
本文关注NSObject类以及Objective-C程序如何与运行时系统交互。特别是,它检查了在运行时动态加载新类并将消息转发到其他对象的范式。它还提供了有关如何在程序运行时查找对象信息的信息。
您应该阅读此文档,以了解Objective-C运行时系统如何工作以及如何利用它。然而,通常情况下,编写可可应用程序时,您不需要知道和理解这些材料。

Runtime 介绍

  • 运行时是相对于编译时来讲的
  • 编译时, 主要是做一些词法语法分析, 好比检查作文中的错别字、语法问题, 也就是编译时类型检查. 编译并不会把代码运行到内存.
  • 运行时, 可执行文件被装载到内存中, 代码跑起来了. 运行时过进行运行时类型检查, 与编译时类型检查不一样, 不是简单的扫描代码. 而是在内存中做些操作, 做些判断.

方法的本质

调用类本身的方法

首先我们通过断点查看汇编代码:

@interface LVPerson : NSObject
- (void)personFun;
@end

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

@interface LVTeacher : LVPerson
@end

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

// 探索方法调用本质
void testCase1(void) {
    LVTeacher *p = [LVTeacher alloc];
    [p personFun];
    [p teacherFun];
}

我们发现方法调用统一走的call --> objc_msgSend.

objc_demo`testCase1:
    0x1000012c0 <+0>:  push   rbp
    0x1000012c1 <+1>:  mov    rbp, rsp
    0x1000012c4 <+4>:  sub    rsp, 0x10
    0x1000012c8 <+8>:  mov    rax, qword ptr [rip + 0x33b9] ; (void *)0x0000000100004710: LVTeacher
    0x1000012cf <+15>: mov    rsi, qword ptr [rip + 0x328a] ; "alloc"
    0x1000012d6 <+22>: mov    rdi, rax
    0x1000012d9 <+25>: call   qword ptr [rip + 0x2d91]  ; (void *)0x00007fff204b4d00: objc_msgSend
    0x1000012df <+31>: mov    qword ptr [rbp - 0x8], rax
    0x1000012e3 <+35>: mov    rax, qword ptr [rbp - 0x8]
    0x1000012e7 <+39>: mov    rsi, qword ptr [rip + 0x327a] ; "personFun"
    0x1000012ee <+46>: mov    rdi, rax
    0x1000012f1 <+49>: call   qword ptr [rip + 0x2d79]  ; (void *)0x00007fff204b4d00: objc_msgSend
->  0x1000012f7 <+55>: mov    rax, qword ptr [rbp - 0x8]
    0x1000012fb <+59>: mov    rsi, qword ptr [rip + 0x326e] ; "teacherFun"
    0x100001302 <+66>: mov    rdi, rax
    0x100001305 <+69>: call   qword ptr [rip + 0x2d65]  ; (void *)0x00007fff204b4d00: objc_msgSend

我们使用clang查看OC对象方法调用底层是怎么做的?

    // clang编译:
    LVTeacher *p = ((LVTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LVTeacher"), sel_registerName("alloc"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("personFun"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("teacherFun"));

发现[p teacherFun] --> objc_msgSend(p, sel_registerName("teacherFun")).

也就是说OC对象方法调用本质就是objc_msgSend实现的.

那么我们是不是也可以直接使用objc_msgSend实现OC对象方法调用呢?

发现我们直接使用objc_msgSend会报错:

Implicitly declaring library function 'objc_msgSend' with type 'id (id, SEL, ...)'

需要进行如下操作来解决这个问题:

target --> Build Setting --> 将Enable strict checking of obc_msgSend calls改为NO
 
将严厉的检查机制关掉

让我们一起测试一下下面的代码, 可发现objc_msgSend(p, sel_registerName("teacherFun"))效果跟实际应用开发中的方法调用[p teacherFun]一样.

// 测试直接调用 objc_msgSend
// [p teacherFun]  vs  objc_msgSend(p, sel_registerName("teacherFun"))
void testCase2(void) {
    LVTeacher *p = [LVTeacher alloc];
    [p personFun];
    [p teacherFun];
    
    NSLog(@"-----");
    
    objc_msgSend(p, sel_registerName("personFun"));
    objc_msgSend(p, sel_registerName("teacherFun"));
}

打印结果

LVPerson -- -[LVPerson personFun]
LVTeacher -- -[LVTeacher teacherFun]
-----
LVPerson -- -[LVPerson personFun]
LVTeacher -- -[LVTeacher teacherFun]

我们发现[p teacherFun]是调用自己的方法, 这点没错; 但是问题来了[p personFun]不是自己的方法, 而是他爹(superclass继承链)LVPerson的对象方法, 怎么也能正常调用了?

继续研究, 我们又发现objc_msgSendSuper可以实现对象调用其父类方法.

调用父类方法

// 测试: 调用父类方法
void testCase3(void) {
    LVTeacher *teacher = [LVTeacher alloc];
    [teacher personFun];

    struct objc_super lv_super;
    lv_super.receiver = teacher;                // 消息的接收者还是自己 teacher
    lv_super.super_class = [LVPerson class];    // 告诉父类是我爹 LVPerson
    objc_msgSendSuper(&lv_super, sel_registerName("personFun"));
}

打印结果:

LVPerson -- -[LVPerson personFun]
LVPerson -- -[LVPerson personFun]

补充 objc_super的结构, 包含接受者receiver, 父类指针super_class.

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

总结:

testCase2() 中可以通过[p personFun]-->objc_msgSend(p, sel_registerName("personFun"))调用父类方法.
testCase3() 中也可以通过objc_msgSendSuper(&lv_super, sel_registerName("personFun"))对父类的方法调用.

于是我们猜想调用方法可能是, 自己的这里找不到会从找父类那里找?

带着这个猜想, 我们展开了下面的研究.

objc_msgSend 缓存查找流程 -- arm64

为了提升性能, 使用汇编代码实现.


objc_msgSend缓存查找流程.png

脱离代码, 如果自己去实现objc_msgSend 缓存查找流程, 大致步骤应该是这样的:

objc_msgSend(receiver, _cmd);

1.通过对象receiver获取isa, 得到class
2.class -> cache
3.cache -> _bucketsAndMaybeMask
4._bucketsAndMaybeMask & bucketsMask -> buckets指针
5._bucketsAndMaybeMask >> 48 -> mask
6._cmd 通过哈希算法得到开始查找下标 first_probed
7.向前遍历查找 [0 <-- first_probed] 区间, 命中则调用imp
8.向前遍历查找 (first_probed <-- mask] 区间, 命中则调用imp
9.否则Miss

你可能感兴趣的:(消息发送机制-快速查找)