iOS 底层原理 - 方法的本质objc_msgSend分析

Runtime的介绍

要看方法的本质先简单介绍一下Runtime。Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发,也就是Runtime。Runtime 是一套由C,C++ ,汇编写成的一套api,为OC提供运行时功能。为什么不用 OC 呢,这是因为对我们编译器来说,OC 属于更高级的语言,相比于 C 和 C++ 以及汇编,执行效率更慢,而在运行时系统需要尽可能快的执行效率。

Runtime的版本

Runtime其实有两个版本: “modern” 和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime的使用方式

Runtime的使用方式分为三种:
1.Objective-C code @selector()
2.NSObject的方法 NSSelectorFromString()
3.sel_registerName 函数api


屏幕快照 2020-02-17 下午4.51.56.png

方法的本质探索

在项目中新建一个对象LGPerson,在main.m中输入以下代码

void run(){
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayNB];
        
        // 发送消息 : objc_msgSemd
        // 对象方法 - person - sel
        // 类方法  - 类 - sel
        // 父类 : objc_superMSgSend
        //
        
        run();
    }
    return 0;
}

通过clang命令(clang -rewrite-objc main.m -o main.cpp)生成main.cpp文件,发现在main.cpp文件中转化为

void run(){
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_5s_4100t0cd5rn_d7gx0n5wqh8w0000gn_T_main_c10e99_mi_0,__func__);
}

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("sayNB"));imp -  函数
        run(); -- run 指针
    }
    return 0;
}

从上面我们很清楚的看到 , run 函数在编译期就确定了函数调用 以及实现 . 而 OC 方法被编译成调用objc_msgSend 函数. 这也就是我们在 Runtime 所提到的 消息发送机制 .通过编译后代码我们看到 objc_msgSend 函数有两个参数 id , SEL . id 显然就是操作哪个对象 . 而通过SEL 与 imp 的机制 , 以此实现了动态调用方法的本质 . 我们称这种机制为 消息发送 。

方法发送的几种情况

调用对象实例方法

LGStudent *s = [LGStudent new];
[s sayCode];
// 方法调用底层编译
// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
objc_msgSend(s, sel_registerName("sayCode"));

调用类方法

 objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));

调用父类实例方法

// 向父类发消息(对象方法)
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));

调用父类类方法

struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

objc_msgSend

objc_msgSend是用汇编写的

一是因为在C语言中不可能通过写一个函数来保留未知的参数并且跳到一个任意的函数指针。C语言没有满足做这件事的必要特性
另一个原因是汇编更容易被机器识别,objc_msgSend必须够快。

objc_msgSend源码分析

打开源码,全局搜索objc_msgSend,在objc-msg-arm64.s中找到入口 ENTRY,如下图所示:


屏幕快照 2020-02-17 下午6.12.34.png
#endif

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    // person - isa - 类
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS

Arm64 下有 31 个通用寄存器
x0 - x7 表示参数
x0 作为返回值的接受参数

快速流程分析

END_ENTRY _objc_msgSend
对消息接受者 (id self, sel _cmd) 判断处理
taggedPointer 判断处理
GetClassFromIsa_p16 isa 指针处理-class
CacheLookup 查找缓存
cache_t 处理 bucket 以及内存哈希处理
6.1:找不到递归下一个 bucket
6.2:找到了就返回 {imp, sel}=*bucket->imp
6.3:遇到意外就重试
6.4:找不到就 JumpMiss
__objc_msgSend_uncached 告诉我们找不到缓存 imp
STATIC_ENTRY __objc_msgSend_uncached
MethodTableLookup 方法查找列表
9.1: save parameter registers: x0..x8, q0..q7 保存参数到寄存器
9.2:self 以及 _cmd 准备
9.3:__class_lookupMethodAndLoadCache3 调用对应oc方法lookUpImpOrForward
__class_lookupMethodAndLoadCache3的方法实现为:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

总结

在编译期由 LLVM 将方法调用编译成调用 objc_msgSend 等函数 , 然后在汇编代码执行缓存查找 sel 对应的 imp , 找到就会返回调用 , 找不到则进入消息查找和消息转发慢速流程 .

你可能感兴趣的:(iOS 底层原理 - 方法的本质objc_msgSend分析)