Objective-C 之 objc_msgSend 简单实现

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 694评论 0 2
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 771评论 0 4
  • 前言 runtime其实在我们日常开发过程中很少使用到,尤其是像我现在比较初级的程序猿就更用不到了。但是去面试很多...
    WolfTin阅读 568评论 0 2
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,109评论 0 7