005-runtime源码分析-汇编

271 阅读4分钟

1. objc_msgSend发送消息,怎么查找方法的

答: 有两种方式进行查找

  • 快速方式:缓存找 -> 汇编cache_t -> imp哈希表 没找到则开始慢速方式查找

  • 慢速方式:C -> 找到-> 缓存

  • 题外问答:为啥objc_msgSend 是使用汇编编写的 答: 原因1: C语言,不可能通过写一个函数保留未知的参数,然后跳转到任意的指针, 而汇编可以 (寄存器)x0, x31

参数是未知的,都是在运行时才能获知参数 原因2: 汇编快

2. 源码分析objc_msgSend流程

发送消息 objc_msgSend, 开始运行汇编编写的objc_msgSend方法
看源码

#endif
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, 1, x0 // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq LReturnZero // nil check
    GetTaggedClass
    b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

看源码分析,ENTRY进入_objc_msgSend方法之后,然后会先判断
是否为空nil check 如果为空,return 然后判断是否tagged pointer
// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged

之后进入LNilOrTagged方法,如果是tagged pointer 直接return
如果不是,在LNilOrTagged方法中进入处理isa的方法 LGetIsaDone
处理isa

看源码

LGetIsaDone:
// calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

CacheLookup 开始从缓存中查找方法 (参数模式 NORMAL)

现在搜索CacheLookup看看内部是怎么实现的 然后就看到了,一个很详细的的对CacheLookup方法的备注,
CacheLookup的查找方式有三种
分别为:NORMAL|GETIMP|LOOKUP image.png

再次看源码吧

1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
    cmp p9, p1                      //     if (sel != _cmd) {
    b.ne 3f                         //         scan more
                                    //     } else {
2: CacheHit \Mode                  // hit:    call or return imp
                                   //     }
3: cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10                  // } while (bucket >= buckets)
    b.hs 1b
    // wrap-around:
    //   p10 = first bucket
    //   p11 = mask (and maybe other bits on LP64)
    //   p12 = _cmd & mask
    //
    // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
    // So stop when we circle back to the first probed bucket
    // rather than when hitting the first bucket again.
    // Note that we might probe the initial bucket twice
    // when the first probed slot is the last entry.

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
                                // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                                // p13 = buckets + (mask << 1+PTRSHIFT)
                                // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p13, p10, p11, LSL #(1+PTRSHIFT)
                                // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

add p12, p10, p12, LSL #(1+PTRSHIFT)
                                    // p12 = first probed bucket
                                    // do {

4: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
   cmp p9, p1                        //     if (sel == _cmd)
   b.eq 2b                           //         goto hit
   cmp p9, #0                        // } while (sel != 0 &&
   ccmp p13, p12, #0, ne             //     bucket > first_probed)
   b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
    b \MissLabelDynamic

看代码分析得出(源码里面都有注释的😂)
先查找CacheHit,没有找到怎找CacheMiss->__objc_msgSend_uncached,
如果站到则返回,并add 如果还是找不到则进入慢速查找过程 源码里面备注写的很清楚

image.png

接下来我们来看看CacheMiss->__objc_msgSend_uncached的源码
.endmacro
    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    
    STATIC_ENTRY __objc_msgLookup_uncached
    UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    MethodTableLookup
    ret
    END_ENTRY __objc_msgLookup_uncached
    STATIC_ENTRY _cache_getImp
    GetClassFromIsa_p16 p0, 0
    CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
    LGetImpMissDynamic:

看源码有个重要的数据结构用来存方法,他就是MethodTableLookup

他就是MethodTableLookup就不分析了,里面太复杂了

汇编没有查找到就会通过慢速查找,源码体现

image.png 最后走到C语言的方法中 _class_lookupMethodAndLoadCache

image.png

最后附上一张流程图

image.png

image.png

源码地址

问,如果获得方法地址并直接调用改方法

避免动态绑定的唯一办法就是取得方法的地址,并且直接象函数调用一样调用它。当一个方法会被连续调 用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来调用方法就显得很有效。 利用 NSObject 类中的 methodForSelector:方法,您可以获得一个指向方法实现的指针,并可以 使用该指针直接调用方法实现。methodForSelector:返回的指针和赋值的变量类型必须完全一致, 包括方法的参数类型和返回值类型都在类型识别的考虑范围中。

@interface** LGPersoon : NSObject
- (void)fly:(NSString *)where;
- (void)fly:(NSString *)from to:(NSString *)to;
@end

@implementation LGPersoon
- (void)fly:(NSString *)where {
    NSLog(@"LGPersoon---fly--- %@",where);
}
- (void)fly:(NSString *)from to:(NSString *)to {
    NSLog(@"LGPersoon---fly--- %@ --- %@",from,to);
}
@end


LGPersoon *p2 = [[LGPersoon alloc] init];
void (*setter)(id, SEL, NSString*);
setter = (void (*)(id, SEL, NSString*))[p2 methodForSelector: @selector(fly:)];
for (int i = 0; i < 1000; i++) {
    setter(p2, @selector**(fly:), @"锤子");
}
        
void (*setter2)(id, SEL, NSString*,NSString*);
setter2 = (void (*)(id, SEL, NSString*))[p2 methodForSelector: @selector(fly:to:)];
for (int i = 0; i < 1000; i++) {
    setter2(p2, @selector(fly:to:), @"锤子",@"");
}

打印
2022-02-23 14:44:15.593064+0800 runtime-初探[88584:2953435] LGPersoon---fly--- 锤子
2022-02-23 17:43:05.163156+0800 runtime-初探[51871:3277358] LGPersoon---fly--- 锤子 --- 铅笔

方法指针的第一个参数是接收消息的对象(self),第二个参数是方法选标(_cmd)。这两个参数在方 法中是隐藏参数,但使用函数的形式来调用方法时必须显示的给出。 使用 methodForSelector:来避免动态绑定将减少大部分消息的开销,但是这只有在指定的消息被重 复发送很多次时才有意义,例如上面的 for 循环。 注意,methodForSelector:是 Cocoa 运行时系统的提供的功能,而不是 Objective-C 语言本身的功 能。