iOS - objc_msgSend分析

Objective-C 是一个动态语言,在动态中创建类和对象、进行消息传递和转发。想要更好的理解 Objective-C 那就离不开 Runtime(运行时) 。

什么是Runtime?

Runtime是用C、C++、汇编编写的一套为OC提供运行时功能的api

初见objc_msgSend

创建一个Student的类

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *student = [[Student alloc] init];
        [student study]; 
    }
    return 0;
}

在终端使用clang命令将main.m编译成main.cpp

clang -rewrite-objc main.m

此时我们会发现main.m文件的下方会多出一个main.cpp的文件,打开并移动到最下方我们看到如下一段代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Student *student = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("study"));
    }
    return 0;
}

我们发现在student创建和调用study的方法是,都调用了objc_msgSend,那么objc_msgSend到底是什么?其作用又是什么呢?

通过汇编初探objc_msgSend

通过打断点,查看汇编我们发现objc_msgSend的实现是在libobjc动态链接库中(objc4源码)。在libobjc源码库中全局搜索objc_msgSend,发现它的实现是用汇编写的。
下面我们就以arm64系统下的汇编就行分析,全部搜索objc_msgSend找到objc-msg-arm64.s文件,找到ENTRY _objc_msgSend,方法的进入是从ENTRY开始的:


    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0 //arm64下有31位寄存器; 对比0号寄存器是否为空, 如果为空则代表当前接受着没有,如果为空则返回nil

#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    // [x0]是消息接收者
   //如果我们要对student发送消息,我们需要用到用到对象方法时我们就可以通过isa找到类Student,如果是类方法是我们则需要isa来找到元类
    ldr p13, [x0]       // p13 = isa
//通过isa寻找类
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone://查找isa完毕
    CacheLookup NORMAL//读取方法        // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
GetClassFromIsa_p16
.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
//判断当前是否为non-pointer isa 
    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
CacheLookup
.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE]//在cache_t一文中我们提到了要拿到方法缓存需要首地址平移16个字节    // p10 = buckets, p11 = occupied|mask
#if !__LP64__//w11用w是因为mask是32位的,不需要用64位的
    and w11, w11, 0xffff    // p11 = mask
#endif
/**
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
*/
    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  //没找到就继续找2流程        //     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//找到了,再去走3流程,是为了存一份缓存,方便下一次填充缓存
    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
如果缓存没有命中的话会走CheckMiss方法
//CacheLookup NORMAL|GETIMP|LOOKUP 
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __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流程:

MethodTableLookup
//方法存在bits->rw->ro的methodList中
.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方法,当我们在汇编中并搜索不到该方法的实现,通过前面x0..x8, q0..q7的准备工作我们猜想,其可能是为调用C或者C++的方法做准备,当我们全局搜索_class_lookupMethodAndLoadCache3时发现在objc-rutime-new.mm文件中找到了该方法的实现

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

这个函数开始就是我们熟知的C/C++的代码了,我们终于也不用再看汇编部分的代码了,消息发送从快速查找过渡到了慢速查找流程。

总结:

在编译期调用 objc_msgSend 函数 , 在汇编代码执行缓存查找 sel 对应的 imp , 找到就会返回调用 , 找不到则由快速查找过渡到了慢速查找流程。

拓展isKindOfClass和isMemberOfClass
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
        BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
        BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
        BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
        BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}
+ (BOOL)isKindOfClass:(Class)cls;
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

由self获取到元类,如果相等就返回YES,不相等就继续找元类的父类也就是NSObject, [NSObject class]的类明显就是NSObject因此[(id)[NSObject class] isKindOfClass:[NSObject class]]返回值为1,同理[(id)[LGPerson class] isKindOfClass:[LGPerson class]]的类为LGPerson,无论是元类还是元类的父类都不可能为LGPerson,因此此处返回为0;

+ (BOOL)isMemberOfClass:(Class)cls;
+ (BOOL)isMemberOfClass:(Class)cls {
//就是元类和类做对比,如果相等返回1,不想等返回0
    return object_getClass((id)self) == cls;
}

元类和类肯定是不一样的,因此[(id)[NSObject class] isMemberOfClass:[NSObject class]]和[(id)[LGPerson class] isMemberOfClass:[LGPerson class]]返回值皆为0;

- (BOOL)isKindOfClass:(Class)cls;
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

第一步都是对比类,例如: [(id)[NSObject alloc] isKindOfClass:[NSObject class]]第一步就是对比该类是不是NSObject类,如果是直接返回1,明显[NSObject alloc]就是NSObject类,所以此时返回为1,同理 [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]] 返回值也为1;

- (BOOL)isMemberOfClass:(Class)cls;
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

对比cls和[self class]是都相同,和isKindOfClass区别在于, isKindOfClass如果第一步不同的时候还会往父类去查找,容错更高一些,明显[(id)[NSObject alloc] isMemberOfClass:[NSObject class]]和[(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]返回值皆为1;

至此我们可以知道答应的结果应该是:

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