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
再次看源码吧
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
如果还是找不到则进入慢速查找过程
源码里面备注写的很清楚
接下来我们来看看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就不分析了,里面太复杂了
汇编没有查找到就会通过慢速查找,源码体现
最后走到C语言的方法中
_class_lookupMethodAndLoadCache
最后附上一张流程图
问,如果获得方法地址并直接调用改方法
避免动态绑定的唯一办法就是取得方法的地址,并且直接象函数调用一样调用它。当一个方法会被连续调 用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来调用方法就显得很有效。 利用 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 语言本身的功 能。