5fc68b6d67479c592e30e852c1143d00
iOS 高级之美(十)—— 消息发送

前言: iOS 高级之美 是本人总结了一些工作实际开发研究以及面试重点,围绕底层进行 源码分析 - LLDB 调试 - 源码断点 - 汇编调试,让读者真正感受 Runtime底层之美~😊
目录如下:

iOS 高级之美(一)—— iOS_objc4-756.2 最新源码编译调试
iOS 高级之美(二)—— OC对象底层上篇
iOS 高级之美(三)—— OC对象底层下篇
iOS 高级之美(四)—— isa原理分析
iOS 高级之美(五)—— 类结构分析上篇
iOS 高级之美(六)—— malloc分析
iOS 高级之美(七)—— 24K纯技术男~KC_2019年终总结
iOS 高级之美(八)—— 类结构分析中篇
iOS 高级之美(九)—— 类结构分析下篇
iOS 高级之美(十)—— 消息发送

一、方法的本质

介绍完Objective-C的基础数据结构,接下来就是方法的调用了。在讨论“消息”之前。我们先来思考一下,我们总是在说在objc中,调用方法实际上是发送消息,那么这个消息的机制是什么? 函数调用的本质应该是什么?我们可以通过一段C的代码来看下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson new];
    }
    return 0;
}

通过终端我们进行探索一下 : clang -rewrite-objc main.m

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
    }
    return 0;
}
  • 底层 C++ 代码更加接近本质
  • 里面有很多的类型转换,为了方便查阅我简化一下 : objc_msgSend(objc_getClass("LGPerson"), sel_registerName("new"))
  • 就是通过 objc_msgSend 函数调用, 这种函数调用的方式也称为发送消息

接下来我们再看下面的分析

void func(){
}
int main(int argc, char * argv[]) {
    func();
}

这里是一个非常简单的函数,下面通过clangobjdump命令来生成可执行文件并查看汇编实现

我们可以看到,函数的调用,实际上就是“地址”的跳转。而函数的地址是在编译期就能确定的(除一些动态库函数外),关于如何在编译期确定地址可以查看之前的静态链接文章

但是Objective-C是一门动态性语言,如果直接使用编译得到的地址,就不存在动态性,那么Objective-C是如何解决这个问题的呢

二、objc_msgSend分析

2.1 objc_msgSend 的结果分析

此汇编结果为iOS13.2.3真机版本的runtime。可参考arm64汇编文档

libobjc.A.dylib`objc_msgSend:
    //判断self 是否 = nil
->  0x1810aa080 <+0>:   cmp    x0, #0x0                  ; =0x0
    //如果是0或者小0,则跳转至 0x1810aa0f8 处
    0x1810aa084 <+4>:   b.le   0x1810aa0f8               ; <+120>

    //读取x0的“值”存入x13中。x13=isa    
    0x1810aa088 <+8>:   ldr    x13, [x0]

    //x16 = x13 & isamask  
    //xl6 = cls     
    0x1810aa08c <+12>:  and    x16, x13, #0xffffffff8

    //读取x16偏移16字节的值,存入 x11
    //x11 = cache_t和mask_t共同组成,前48位为cache_t
    0x1810aa090 <+16>:  ldr    x11, [x16, #0x10]

    /*  CacheLookup */
    //苹果开源的runtime(mac10.14)中,cache存储在cls偏移16字节处    
    //但在目前的版本中(10.15),cache_t会和mask_t公用这8个字节。使用0xffffffffffff获取cache_t。
    //x10 = bucket_t = (x11 & 0xffffffffffff)
    0x1810aa094 <+20>:  and    x10, x11, #0xffffffffffff

    //x11 >> 48获取mask_t,  
    //x12 = mask_t & SEL = cache_hash(buckets的索引)
    0x1810aa098 <+24>:  and    x12, x1, x11, lsr #48

    //获取索引值相对buckets首地址的偏移量,cache_hash << 4,因为每个bucket占16字节,所以把索引 * 16;
    //x12:取索引对应的bucket = buckets首地址+索引偏移量
    0x1810aa09c <+28>:  add    x12, x10, x12, lsl #4

    //读取bucket的内容,分别存入 x17,x9
    //x17 = imp,  x9 = cache_key(SEL)
    0x1810aa0a0 <+32>:  ldp    x17, x9, [x12]

    //cachekey == SEL
    0x1810aa0a4 <+36>:  cmp    x9, x1

    //cachekey != SEL,跳转到 0x1810aa0b4
    0x1810aa0a8 <+40>:  b.ne   0x1810aa0b4               ; <+52>


    /*  CacheHit */
    0x1810aa0ac <+44>:  eor    x17, x17, x16    
    //直接执行cache中的IMP
    0x1810aa0b0 <+48>:  br     x17

    /*  CheckMiss */    
    //调用x9 是否为0 。如果为0 ,这说明这个bucket是空桶,就跳转 _objc_msgSend_uncached->lookupIMPOrForward
    0x1810aa0b4 <+52>:  cbz    x9, 0x1810aa3c0           ; _objc_msgSend_uncached

    //对比对应出的bucket_t 是否等于 buckets首地址
    0x1810aa0b8 <+56>:  cmp    x12, x10

    //如果相等,说明找了一圈,仍没有找到,则跳转至 0x1810aa0c8。
    0x1810aa0bc <+60>:  b.eq   0x1810aa0c8               ; <+72>

    //bucket_t的地址详情偏移16,相当于 取当前bucket_t的前一个元素。
    //把imp和key分别赋值给 x17 x9
    0x1810aa0c0 <+64>:  ldp    x17, x9, [x12, #-0x10]!

    //跳转回0x1810aa0a4,相当于 向前循环遍历查找buckets
    0x1810aa0c4 <+68>:  b      0x1810aa0a4               ; <+36>

    //到这里说明 x12 是buckets中的第一个bucket。
    //x11 为mask和buckets,这里右移 44位,说明了buckets实际只是用了 44bit而不是48
    //这里相当于把x12重新指向buckets的最后一个bucekt。
    //x12 = buckets->last
    0x1810aa0c8 <+72>:  add    x12, x12, x11, lsr #44

    //将bucket的imp和key 写入 x17 和 x9
    0x1810aa0cc <+76>:  ldp    x17, x9, [x12]

    //对比 SEL == key
    0x1810aa0d0 <+80>:  cmp    x9, x1

    //不相等 跳转到 0x1810aa0e0,判断cache_key 是否为空
    0x1810aa0d4 <+84>:  b.ne   0x1810aa0e0               ; <+96>

    /*  CacheHit */
    //如果相等则
    0x1810aa0d8 <+88>:  eor    x17, x17, x16
    //执行x17:IMP
    0x1810aa0dc <+92>:  br     x17

    /*  CheckMiss */    
    //如果此时 x9 == 0 则执行_objc_msgSend_uncached
    0x1810aa0e0 <+96>:  cbz    x9, 0x1810aa3c0           ; _objc_msgSend_uncached

    //对比对应出的bucket_t 是否等于 buckets首地址
    0x1810aa0e4 <+100>: cmp    x12, x10

    //如果等于则跳转0x1810aa0f4 -> _objc_msgSend_uncached
    0x1810aa0e8 <+104>: b.eq   0x1810aa0f4               ; <+116>

    //不相等则 向前查找
    0x1810aa0ec <+108>: ldp    x17, x9, [x12, #-0x10]!
    //跳转0x1810aa0d0
    0x1810aa0f0 <+112>: b      0x1810aa0d0               ; <+80>
    //跳转_objc_msgSend_uncached
    0x1810aa0f4 <+116>: b      0x1810aa3c0               ; _objc_msgSend_uncached

    //msgSeng刚开始的第二行判断,这里是nil或者tagged
    0x1810aa0f8 <+120>: b.eq   0x1810aa130               ; <+176>
    0x1810aa0fc <+124>: adrp   x10, 294986
    0x1810aa100 <+128>: add    x10, x10, #0x340          ; =0x340 
    0x1810aa104 <+132>: lsr    x11, x0, #60
    0x1810aa108 <+136>: ldr    x16, [x10, x11, lsl #3]
    0x1810aa10c <+140>: adrp   x10, 294986
    0x1810aa110 <+144>: add    x10, x10, #0x2a8          ; =0x2a8 
    0x1810aa114 <+148>: cmp    x10, x16
    0x1810aa118 <+152>: b.ne   0x1810aa090               ; <+16>

    // ext tagged
    0x1810aa11c <+156>: adrp   x10, 294986
    0x1810aa120 <+160>: add    x10, x10, #0x3c0          ; =0x3c0 
    0x1810aa124 <+164>: ubfx   x11, x0, #52, #8
    0x1810aa128 <+168>: ldr    x16, [x10, x11, lsl #3]
    0x1810aa12c <+172>: b      0x1810aa090               ; <+16>
    0x1810aa130 <+176>: mov    x1, #0x0
    0x1810aa134 <+180>: movi   d0, #0000000000000000
    0x1810aa138 <+184>: movi   d1, #0000000000000000
    0x1810aa13c <+188>: movi   d2, #0000000000000000
    0x1810aa140 <+192>: movi   d3, #0000000000000000
    0x1810aa144 <+196>: ret    
    0x1810aa148 <+200>: nop    
    0x1810aa14c <+204>: nop    
    0x1810aa150 <+208>: nop    
    0x1810aa154 <+212>: nop    
    0x1810aa158 <+216>: nop    
    0x1810aa15c <+220>: nop   

这里没有分割开来 ,主要是为那些想连贯读下来的小伙伴分析!

整个objc_msgsend 可分为四个阶段:

  • ①. 准备工作,包括获取类对象mask_tcash_hash(查找索引)buckets

  • ②. 获取cash_hash所在bucketsbucket,并向前遍历查找,这里有3中情况:

    • 1. 如果找到则调用IMP
    • 2. 如果找到的bucket.key == 0,即bucket为空桶,则调用 objc_msgSend_uncached
    • 3. 如果找到第一个bucket,则跳转到3。
  • ③. 从`buckets`的最后一个向前查找:
    • 1. 如果找到则调用IMP
    • 2. 如果找到的bucket.key == 0,即bucket为空桶,则调用 objc_msgSend_uncached
  • ④. 调用objc_msgSend_uncached

2.2 伪代码

由于汇编看起来不是很直观,可直接参考下面的伪代码,同时这部分伪代码没有处理关于参数和返回值的逻辑。

void discover_msgSend(id self,SEL sel1){
    if (self == nil) {
        return;
    }
    SEL sel = sel1;
    Class clsHasMask = *(Class *)self;
    Class cls = (Class)((uintptr_t)clsHasMask & 0xffffffff8);
    struct cache_t * cache = (struct cache_t *)((__bridge void *)cls + 0x10);

    struct bucket_t *_buckets = (struct bucket_t *)((uintptr_t)cache->_buckets & 0xffffffffffff);
    uintptr_t mask = (uintptr_t)cache->_buckets >> 48;
    uintptr_t cache_hash = (uintptr_t)sel & mask;
    struct bucket_t* bucket = (_buckets + cache_hash*1);//cache_hash*16
    /*  CacheLookup */
    while ((*bucket)._key != (uintptr_t)sel) {
        /*  CheckMiss */
        if ((*bucket)._key == 0) {
            goto _objc_msgSend_uncached;
        }
        /*  查找到第一个元素了 */
        if (bucket == _buckets) {
            goto second_loop;
        }
        bucket = (bucket-1);//[x12, #-0x10]
    }
    if(((*bucket)._key) == (uintptr_t)sel){
        IMP x16 = ((uintptr_t)(*bucket)._imp) ^ (uintptr_t)cls;
        x16();
    }else{
        goto _objc_msgSend_uncached;
    }
    return;

second_loop:
    {
        int64_t offset = ((uintptr_t)cache->_buckets >> 44);
        struct bucket_t* lastBucket = (_buckets + offset/16);
        /*  CacheLookup */
        while ((*lastBucket)._key != (uintptr_t)sel) {
            /*  CheckMiss */
            if ((*lastBucket)._key == 0) {
                goto _objc_msgSend_uncached;
            }
            lastBucket = (lastBucket-1);//[x12, #-0x10]
        }
        if(((*lastBucket)._key) == (uintptr_t)sel){
            IMP x16 = ((uintptr_t)(*lastBucket)._imp) ^ (uintptr_t)cls;
            x16();
        }else{
            /*  JumpMiss */
            goto _objc_msgSend_uncached;
        }
        return;
    }
_objc_msgSend_uncached:
    {
        printf("\n _objc_msgSend_uncached -> lookUpImpOrForward");
        return;
    }
}

通过上面的流程我们发现,objc_msgSend会有两次的查找行为,这又是为什么呢?我们可以通过runtime的源码中找到答案。

Clone scanning loop to miss instead of hang when cache is corrupt.

top Created with Sketch.