iOS 深入理解程序员iOS Developer

Objective-C 之 objc_msgSend 简单实现

2017-04-04  本文已影响295人  要上班的斌哥

objc_msgSend 函数

在 Objective-C 中, message 是直到 runtime 的时候才会绑定实现,编译器会将我们的发送消息 [receiver message] 变成函数调用 objc_msgSend,该函数会有2个默认参数,分别是 receiver 和 selector 。当然,若是该函数有其他的参数,那么其他参数就跟在2个默认参数后面。

objc_msgSend(receiver, selector, arg1, arg2, ...)

在动态绑定过程中,objc_msgSend 函数的作用如下

  1. 找到 selector 对应的方法实现,
  2. 调用方法实现,传入参数
  3. 接收方法实现的返回值并将这个返回值作为自己的返回值

objc_msgSend 函数调用过程中依赖于编译器为每个类和对象提供的一些类结构,这个类结构包含了下面 2 个基本元素

  1. 一个指向 superclass 的指针, 这个指针叫做 isa,对象可以用它来来访问 class ,通过 class 可以访问这个 class 类继承层次上的所有其他 class
  2. 一个 class dispatch table , 一个 selector - 方法实现地址 对应的表格, 比如 sestOrigin:: 的 selector 对应的是 setOrigin::的方法实现地址
Messaging Framework

Messaging 过程如上图所示,当一个对象收到一个消息(message)的时候,objc_msgSend 函数(messaging function) 会根据对象的 isa 指针 到 class dispatch table 里面去查找 method selector 。如果找不到呢?那就根据 isa 指针寻找到 superclass ,若是一直没有找到,那么就会沿着类继承层次来到了 NSObject 。一旦找到了 method selector,那么就调用 method selector 对应的方法实现并传入对应的参数。这就是 runtime 寻找方法实现的方式,消息动态绑定到方法实现。

为了提高 Messaging 过程的速度,消息被首次使用的时候,runtime 会缓存 selector 和 方法实现地址。Messaging 的时候会先查询缓存,若是没有缓存,那么去 class dispatch table 寻找,若是有缓存,那么直接使用缓存,这个时候 Messaging 的速度只是比直接的函数调用来的慢那么一点点儿,这个差异基本可以忽略。

runtime 的核心在于消息分派器 objc_msgSend,它的作用是把选择器映射为函数指针,并调用被函数指针引用的函数。我们来做简单的 objc_msgSend 的实现。

C 代码实现消息分派器

static const void *myMsgSend(id receiver, const char *name){
    // selector 是选择器,是方法名的唯一标识符
    SEL selector = sel_registerName(name);
    // methodIMP 方法实现,只是一个指向方法某个函数的指针
    IMP methodIMP = class_getMethodImplementation(object_getClass(receiver), selector);
    // 执行 methodIMP 方法实现,methodIMP 接受一个对象,一个选择器,可变长参数列表作为参数,并返回一个对象
    return methodIMP(receiver,selector);
}

定义一个 myMsgSend 函数,该函数的作用类似于 objc_msgSend。

  1. 使用 sel_registerName 获取方法的唯一标识符
  2. 使用 class_getMethodImplementation 获取关于某个类或者对象的方法实现
  3. 给对应得方法实现 methodIMP 传入合适的参数,并返回一个对象。

接下来我们看看如何使用这个简单的 objc_msgSend 实现。

void RunMyMsgSend(){

    // NSObject *object = [[NSObject alloc] init];
    // objectClass 定义了 Objective-C 的类
    Class objectClass = (Class)objc_getClass("NSObject");
    id object = class_createInstance(objectClass, 0);
    myMsgSend(object, "init");
    
    // id description = [object description];
    id description = (id)myMsgSend(object, "description");
    
    // const char *cstr = [description UTF8String];
    const char *cstr = myMsgSend(description, "UTF8String");
    printf("%s\n",cstr);
}

从代码中可以看到,我们将 Objective-C 的发送消息过程 ,使用 C 语言来做了简单的类似实现。可以粗略的说,runtime 真的只是 C 。

  1. NSObject *object = [[NSObject alloc] init] 发送 init 消息使用 myMsgSend(object, "init") 实现
  2. id description = [object description] 发送 description 消息使用 myMsgSend(object, "description") 实现
  3. [description UTF8String] 发送 UTF8String 消息使用 myMsgSend(description, "UTF8String") 实现

运行以下代码

printf("\n\nRunMyMsgSend()\n");
RunMyMsgSend();
NSObject *testObj = [[NSObject alloc] init];
NSLog(@"%@", testObj);

输出运行结果

RunMyMsgSend()
<NSObject: 0x60800000fd50>
<NSObject: 0x60800000fd60>

methodForSelector: 实现消息分派器

在 Objective-C 中,我们可以使用 methodForSelector: 来实现消息分派,那么使用 methodForSelector: 和 使用 objc_msgSend相比,会有性能提升么?为了这个性能提升直接跳过objc_msgSend值得么? 直接用代码来个测试。

const NSUInteger kTotalCount = 100000000;
typedef void (*voidIMP)(id,SEL,...);

// 分别使用objc_msgSend 和 methodForSelector 来做消息分派,对字符串做一亿次操作之后进行耗时比较
void FastCall(){
    
    NSMutableString *string = [NSMutableString string];
    NSTimeInterval totalTime = 0;
    NSDate *start = nil;
    NSUInteger count = 0;
    
    // 用 objc_msgSend
    start = [NSDate date];
    for (count = 0; count < kTotalCount; count ++) {
        [string setString:@"stuff"];
    }
    // 计算用 objc_msgSend 耗时时间
    totalTime = -[start timeIntervalSinceNow];
    printf("w/ objc_msgSend = %f\n", totalTime);

    // 跳过 objc_msgSend, 使用 methodForSelector 来做消息分派
    start = [NSDate date];
    SEL selector = @selector(setString:);
    voidIMP setStringMethod = (voidIMP)[string methodForSelector:selector];
    for (count = 0; count < kTotalCount; count ++) {
        setStringMethod(string,selector,@"stuff");
    }
    // 计算用 跳过 objc_msgSend 耗时时间
    totalTime = -[start timeIntervalSinceNow];
    printf("w/ objc_msgSend = %f\n",totalTime);
}

多次运行之后,结果如下

用 objc_msgSend 耗时 = 6.885367
跳过 objc_msgSend 耗时 = 6.475993

可以为这个例子做个总结。多数情况下,我们会把 Objective-C 的方法重写成函数,这样可以得到更好更可靠的性能提升,但是不要想着绕过消息分派器 objc_msgSend ,因为 objc_msgSend 的性能开销已经小到可以忽略不计了,不需要再去优化这个过程了

那么什么时候需要绕过消息分派器 objc_msgSend 呢?那就是循环内的大量方法调用,我们以一个<code>setFilled: </code>方法来做示例。

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

参考

本文是《iOS 编程实战》的读书笔记,对阅读的内容进行总结。当我们看懂了之后,不一定懂;我们跟着书上代码敲了一遍之后,还是不一定懂;只有我们能够把自己理解的内容写下来或者通过其它方式表达出来的时候,这个才是真的懂了;

《iOS编程实战》第二十四章 深度解析Objective-C https://book.douban.com/subject/25976913/
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtHowMessagingWorks.html

上一篇 下一篇

猜你喜欢

热点阅读