OC消息转发(一)— objc_msgSend探索

前言

该系列我们来探究一下OC的消息发送和转发机制,本文我们就来对objc_msgSend做一下初步探索,明白方法调用是如何快速寻找到方法的。以后我们会探索到慢速寻找方法以及找不到方法是如何进行消息转发的。

runtime简介

要探索objc_msgSend,我们首先要了解runtimeruntimeCC++、汇编混合写成的一套为Objective-C提供运行时功能的API。也是因为runtimeObject-C才被成为动态语言。

runtime的版本

runtime的版本分为两个版本modernlegacy官方文档),我们现在使用的Objective-C 2.0版本就是modern版本,只能适用于iOS64 bit OS X 10.5版本及更高版本;legacy则适用于其他版本和32 bit OS Xmodernlegacy最大的区别就是如果更改类中实例变量的布局,legacy需要重新编译他的子类,modern版本则不需要。

runtime的使用

runtime的使用大致可分为三种使用方法。

  • Objective-Ccode:@selector()等;
  • NSObject的方法:NSSelectorFromString()等;
  • runtimeapisel_registerName()等;
编译时和运行时

编译时:即编译器对语言的编译阶段,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味着程序就可以成功运行。
运行时:即程序通过了编译这一关之后编译好的代码被装载到内存中跑起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码的简单扫描分析,此时若出错程序会崩溃。这个阶段也是runtime起作用的阶段。

objc_msgSend探索

一、clang生成cpp文件
创建工程,在main.m写入以下代码:

void run(){
    NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建LGPerson类和对象方法sayNB
        LGPerson *person = [LGPerson alloc];
        [person sayNB];
        run();
    }
    return 0;
}

打开终端进入main.m文件目录下,执行以下命令:

clang -rewrite-objc main.m -o main.cpp

在此文件夹下会生成一个main.cpp文件,打开文件滑动到底部可以看到如下代码:

void run(){
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_85_h8yymn657hq3vfgnz_xwbtjc0000gp_T_main_26fe1b_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"));

        run();
    }
    return 0;
}

从代码中可以看出,调用allocsayNB两个方法被转换成了objc_msgSend发送消息((void (*)(id, SEL))(void *)是类型强转),而我写的一个run()函数则是直接调用,不是通过objc_msgSend进行消息发送,由此可以看出只有Objective-C的方法是通过runtime转换为消息发送的。

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

objc_msgSend的两个参数idsel代表消息接收者和方法唯一标识。
二、断点看汇编
sayNB处打断点,如图:

断点sayNB

进入断点,然后菜单 Debug -> Debug Workflow -> Always Show Disassembly,显示汇编如下:

objc_msgSend汇编

可以看到objc_msgSend,然后按着control+进入objc_msgSend详情,如下:

objc_msgSend详情

可已看出objc_msgSend是在libobjc里边,接下来我们去找源码看看objc_msgSend是如何快速进行方法查找的。
三、objc_msgSend汇编源码
objc_msgSend源码是用汇编写的,全局搜索objc_msgSend找到汇编(文件表示上为sarm64文件,ENTRY _objc_msgSend是开始如下:

寻找objc_msgSend

objc_msgSend汇编源码如下:

    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
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

可以看出先进行了niltagged pointer的检测,SUPPORT_TAGGED_POINTERSarm64下为1,ldr p13, [x0]把在[x0]位置的isa存入p13中,GetClassFromIsa_p16 p13通过isa获取class,GetClassFromIsa_p16详情如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

isa指针详解文章中SUPPORT_INDEXED_ISA在iOS设备上是0,那么进入and p16, $0, #ISA_MASK中,也即是通过掩码ISA_MASKisa获取类信息。
接下来全局搜索CacheLookup,找到带有.macro的宏定义,是CacheLookup详情。如下:

.macro CacheLookup
    // p1 = SEL, p16 = class
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

参考类的结构分析,分析上方汇编代码:

注意:pwx的区别
p16代表指针;w16代表32位下的值,4字节;x16代表64位下的值,8字节;

  • ldp p10, p11, [x16, #CACHE]:全局搜索define #CACHE会发现#CACHE是16,通过GetClassFromIsa_p16可以知道x16代表class,对象结构里内存平移16位(isasuperclass)可以得到cachecache又包含了_buckets_mask_occupied。这句汇编的意思就是把_buckets存入到p10,把_mask_occupied存入到p11,又因为是小端模式,p11 = occupied|mask

  • and w12, w1, w11:这里用w是因为代表8字节只取4字节,即w11=maskw1sel转换之后的keyw12存储的是key&mask即方法在哈希表的索引值。

  • add p12, p10, p12, LSL #(1+PTRSHIFT)p10buckets的首地址,而bucket_t结构体占用16字节,所以buckets的首地址加上索引向左偏移(1+PTRSHIFT)字节得到的值就是函数方法在缓存中的地址。因此p12就是函数方法对应的bucket地址。

  • ldp p17, p9, [x12]:将bucket存放在p17p9中,p17impp9里装sel

  • 1: cmp p9, p1:比较取出来的selp1是否相等,b.ne 2f不相等进入2:CheckMiss $0缓存未命中;相等则是CacheHit $0缓存命中。CacheHit详情如下:

// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    AuthAndResignAsIMP x0, x12  // authenticate imp and re-sign as IMP
    ret             // return IMP
.elseif $0 == LOOKUP
    AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

CacheHit就是找到了imp,那么直接调用TailCallCachedImp就完成了查找。

  • cmp p12, p10:比较p12p10是否相等,相等的话说明进入3f:add p12, p12, w11, UXTW #(1+PTRSHIFT),索引值即为mask;不相等则重新赋值p9,循环进入1f。下方de就是进入到循环查找imp的循环中了。
  • JumpMiss $0:跳转到JumpMiss。如下:
.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

进入NORMAL判断中,调用__objc_msgSend_uncached。如下:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

然后调用MethodTableLookup,如下:

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

最后是调用了__class_lookupMethodAndLoadCache3方法,bl是跳转方法,该方法还带有双下划线,并且搜不到方法的具体实现,可以得出该方法不再是汇编方法,应该是跳转到了C或者C++的方法。
到此我们就把objc_msgSend汇编快速查找方法的探索完了,那为什么要用汇编语言查找方法呢?大概是有两个原因:
1、这个过程需要的是速度,汇编更容易被计算机识别,速度更快。
2、因为方法都会有传参和返回参数,而且是不确定的,相对于C或者C++是很难实现这些的,但是汇编是可以的。

总结

1、Objective-C调用方法是一个通过objc_msgSend发送消息进行查找方法的实现imp的。
2、objc_msgSend查找方法首先是汇编语言查找,这是一个快速的过程。还有一个是慢速查找的过程。

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

推荐阅读更多精彩内容