iOS开发 — OC的方法本质

1,041 阅读2分钟

在iOS的开发中,调用方法可谓再常见不过的事,那么方法的本质究竟是什么?这次就让我们从这个问题入手,探索方法的本质以及方法查找流程和消息转发机制。

方法的本质

新建一个项目,代码如下:

代码

#import <Foundation/Foundation.h>

@interface DZPerson : NSObject

- (void)sleep;

@end

@implementation DZPerson

- (void)sleep{
    NSLog(@"sleep");
}

@end

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

int main(int argc, char * argv[]) {
    @autoreleasepool {
        DZPerson *p = [DZPerson alloc];
        [p sleep]; // OC方法
        go(); // C函数
    }
}

编译

我们通过clang命令编译clang -rewrite-objc main.m -o main.cpp得到编译后的代码,打开main.cpp,直接拉到下面查看main函数实现:

在上图中我们可以清楚的看到,go函数在编译器就确定的函数调用和实现,而OC方法被编译成objc_msgSend函数,因此可以得出结论:OC方法的本质就是通过objc_msgSend等函数来发送消息。

objc_msgSend

我们可以看到objc_msgSend里有两个参数id和SEL,id是消息接受者,SEL是方法编号。

调用对象方法

DZPerson *p = [DZPerson alloc];
[p sleep];
objc_msgSend(p, sel_registerName("sleep"));

调用类方法

objc_msgSend(objc_getClass("DZPerson"), sel_registerName("eat"));

调用父类对象方法

struct objc_super dzSuper;
dzSuper.receiver = p;
dzSuper.super_class = [DZSuper class];
objc_msgSendSuper(&dzSuper, @selector(hello));

调用父类类方法

struct objc_super myClassSuper;
myClassSuper.receiver = [p class];
myClassSuper.super_class = class_getSuperclass(objc_getClass([p class]));
objc_msgSendSuper(&myClassSuper, sel_registerName("hi"));

注意:在项目中使用objc_msgSend函数要把校验关闭,否则会报错:

objc_msgSend源码分析

我们通过查找可以看到:

objc_msgSend的源码很长就不展示了,建议大家自己查看。

我们可以看到objc_msgSend的源码是使用汇编来编写的,个人认为主要原因有以下几种:

  • 性能:高级代码最终会转换成汇编语言来让机器识别,直接使用汇编语言效果会更高。
  • 限制:C、C++语言作为静态语言没有办法能满足个数未知、类型未知的函数。
  • 安全:为了防止系统函数被hook,使用汇编来调用方法和实现函数更为安全。

流程总结

简单总结一下objc_msgSend源码流程:

  1. ENTRY _objc_msgSend开始。
  2. 对消息接受者(id self, SEL _cmd)进行判断处理。
  3. taggedPointer判断。
  4. 通过GetClassFromIsa_p16对isa进行处理得到class。
  5. 通过CacheLookup去查找缓存。
  6. 通过指针偏移找到cache_t,然后找到bucket,通过sel哈希算法之后的key去寻找imp,找到则表示缓存命中(CacheHit)返回imp,找不到则跳到JumpMiss
  7. 通过查看JumpMiss可以一路找到__objc_msgSend_uncached->MethodTableLookup
  8. MethodTableLookup里调用bl __class_lookupMethodAndLoadCache3来到方法的慢速查找流程。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

至此,objc_msgSend的汇编源码流程就分析完毕了,这其实也是方法的快速查找流程。

总结

OC方法会在编译期间被编译成objc_msgSend等函数,然后在底层汇编代码中查找缓存中sel对应的imp,找到了就直接返回,这便是方法的快速查找流程,找不到则进入方法的慢速查找流程。