5196 字
26 分钟
源码解读objc_msgSend
前言
- 本文基于
objc4-818.2源码iOS14.6 iPhoneX
- 源码版本、系统版本、设备不同的话,真机调试的汇编会和源码有不同。自行调试的时候需要注意
- 源码中对于重要部分做了注释,
...表示省略了一些其他架构或者无关紧要的代码 - 为了方便阅读,汇编代码中的做了
缩进调整和分块注释,阅读起来应该会轻松不少 电脑端阅读体验更佳哦- 本文看着会比较累,用了个看着比较轻松愉悦的主题。
- 配上一杯 ☕️ 体验更佳
objc_msgSend相关的内容很多,此文会持续更新、修正- 具体更新内容可以查看
更新记录
- 具体更新内容可以查看
欢迎收藏、交流、抓虫
更新记录
| 更新日期 | 变更 |
|---|---|
| 20210701 | 首次发布 |
一、平时我们是如何调用方法的?
有三种方式进行调用:
OC Method
[model doSomething]NSObject API
[model isKindOfClass:[NSString class]]objc API。
class_getSuperclass([RYModel class])API的层级结构如下:

1.1 实验三种方式
RYModel *obj = [[RYModel alloc] init];[obj dosomethingA];
[obj performSelector:NSSelectorFromString(@"dosomethingA")];
// < Xcode 12// objc_msgSend((id)obj, sel_registerName("dosomethingA"));
// Xcode 12 需要转类型,只改Build setting不行((void (*)(id, SEL))(void *)objc_msgSend)(obj, sel_registerName("dosomethingA"));需要注意的点:
#import <objc/message.h>Xcode 12以下Build setting->objc_msgSend->Enable Strict Checking of objc_msgSend Calls->NO
Xcode 12以上- 需要对 objc_msgSend 转类型,才能正常调用
输出:都能够正常调用
ObjcMsgSend[17313:6255181] -[RYModel dosomethingA]ObjcMsgSend[17313:6255181] -[RYModel dosomethingA]ObjcMsgSend[17313:6255181] -[RYModel dosomethingA]1.2 调用父类方法
@interface RYSubModel : RYModel
- (BOOL)isSub;
@end
@implementation RYSubModel
- (BOOL)isSub { NSLog(@"%s",__FUNCTION__); [super dosomethingA]; return YES;}
@endRYSubModel 继承 RYModel ,我们看看C++文件中 isSub 的情况。
static BOOL _I_RYSubModel_isSub(RYSubModel * self, SEL _cmd) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_py_7v1wvf813z5bmw0hr97yplwc0000gn_T_RYModel_c52934_mi_6,__FUNCTION__); ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("RYSubModel"))}, sel_registerName("dosomethingA")); return ((bool)1);}这里 [super dosomethingA] 转成C++后,发现一个 objc_msgSendSuper 方法,通过它来调用父类的方法。
1.3 objc_msgSendSuper
我们试试能不能直接调用 objc_msgSendSuper。
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)- super
- objc_super 结构体指针
- op
- selector
- super
/// Specifies the superclass of an instance.struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.#if !defined(__cplusplus) && !__OBJC2__ /* For compatibility with old objc-runtime.h header */ __unsafe_unretained _Nonnull Class class;#else __unsafe_unretained _Nonnull Class super_class;#endif /* super_class is the first class to search */};objc_super
/// Specifies the superclass of an instance.struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.#if !defined(__cplusplus) && !__OBJC2__ // /* For compatibility with old objc-runtime.h header */ __unsafe_unretained _Nonnull Class class;#else __unsafe_unretained _Nonnull Class super_class;#endif /* super_class is the first class to search */};代码
struct objc_super ry_super;ry_super.receiver = obj;ry_super.super_class = [RYModel class];((void (*)(id, SEL))(void *)objc_msgSendSuper)((__bridge id)(&ry_super), sel_registerName("dosomethingA"));
输出:ObjcMsgSend[17330:6265149] -[RYModel dosomethingA] // 调用父类方法成功修改父类参数
我们将父类修改成它自己的类对象试试。 ry_super.super_class = [RYSubModel class];,输出依然正确。内部有个查找的链条。
1.4 源码
我们找一下 objc_msgSend 的源码。
ARM64版的源码都在 objc-msg-arm64.s 中,是 汇编 的。
通过添加符号断点也可以查看到汇编代码。
二、汇编说明
2.1 这里用到的主要的的汇编指令备注
| 指令 | 示例 | 备注 |
|---|---|---|
| cmp | cmp x0, #0x0 | 作差值计算,结果丢弃,只调整状态寄存器CPSR标志位 |
| b.le | b.le 0x1afad2b98; | 意为less than or equal to,结果<=0,的时候跳转指令 0x1afad2b98 |
| b.eq | b.eq 0x1afad2b68 | 意为equal,结果==0,时跳转指令0x1afad2b68 |
| and | and x16, x13 | x16 & x13 与操作 |
| ldr | ldr x13, [x0] | 将地址x0的储存器的数据,放入x13寄存器 |
| lsr | lsr #48 | 逻辑右移48位 |
| lsl | lsl #4 | 逻辑左移4位 |
| ldp | ldp x17, x9, [x12] | 取出x12的值存入x17,x9 |
| b.ne | b.ne 0x1afad2b54 | 不相等,跳转指令0x1afad2b54 |
| tbnz | tbnz p11, #0, LLookupPreopt\Function | Test and branch Not zero,即:如果p11的第0位不为0就执行LLookupPreopt\Function |
| TBZ | - | 为0,和tbnz类似 |
| adrp | adrp x0, 1 | 1. 将1的值,左移12位 1 0000 0000 0000 == 0x1000;\n 2.将PC寄存器的低12位清零 0x1045228b0 ==> 0x104522000; 3.将将1 和 2 的结果相加 给 X0 寄存器 |
| eor | - | 异或 |
2.2 寄存器说明
x0-x7,是用来传参的。- x0:消息接收者
- x1:sel
☕️ 休息一下吧,后面开始会有点累
三 _objc_msgSend 源码汇编
ENTRY _objc_msgSendUNWIND _objc_msgSend, NoFrame
// 判断是否存在消息接收者cmp p0, #0 // nil check and tagged pointer check 比较P0寄存器和0
#if SUPPORT_TAGGED_POINTERS// 支持Taggedpointer ARM64下 SUPPORT_TAGGED_POINTERS = 1,// 根据状态寄存器结果 p0 - 0 <= 0 跳转执行 LNilOrTaggedb.le LNilOrTagged // (MSB tagged pointer looks negative)#else// 等于0,即接收者为nil,LReturnZerob.eq LReturnZero#endif
// ISA 存入p13ldr p13, [x0] // p13 = isa
// 从ISA中获取 Class,p16存的是Class的指针GetClassFromIsa_p16 p13, 1, x0 // p16 = class
// 获取ISA完成后执行:LGetIsaDone:// calls imp or objc_msgSend_uncached// 调用IMP或者查缓存CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
END_ENTRY _objc_msgSend3.1 GetClassFromIsa_p16
获取Class存入p16
/******************************************************************** * GetClassFromIsa_p16 src, needs_auth, auth_address * src is a raw isa field. Sets p16 to the corresponding class pointer. * The raw isa might be an indexed isa to be decoded, or a * packed isa that needs to be masked. * * On exit: * src is unchanged * p16 is a class pointer * x10 is clobbered ********************************************************************/.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA...#elif __LP64__.if \needs_auth == 0 // _cache_getImp takes an authed class already mov p16, \src.else // needs_auth == 1 // 64-bit packed isa ExtractISA p16, \src, \auth_address.endif#else // 32-bit raw isa mov p16, \src
#endif
.endmacro3.2 ExtractISA 获取ClassISA
// XS/XS Max/XR A12处理器开始#if __has_feature(ptrauth_calls)// JOP....macro ExtractISA // ISA($1) & ISA_MASK 存入 p16p16($0),即 class and $0, $1, #ISA_MASK#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP xpacd $0#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH mov x10, $2 movk x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48 autda $0, x10#endif.endmacro...
// JOP#else// not JOP
....macro ExtractISA // ISA($1) & ISA_MASK 存入 p16($0),即 class and $0, $1, #ISA_MASK.endmacro
// not JOP#endif3.3 CacheLookup 查找缓存
参数:
- Mode:缓存命中模式
- CacheHit
- Function
- 命中缓存后调用
- MissLabelDynamic
- 未命中调用
- MissLabelConstant
- MissLabelConstant is only used for the GETIMP variant
官方注释
- 建议查看一下,有助于理解
/******************************************************************** * * CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant * * MissLabelConstant is only used for the GETIMP variant. * * Locate the implementation for a selector in a class method cache. * * When this is used in a function that doesn't hold the runtime lock, * this represents the critical section that may access dead memory. * If the kernel causes one of these functions to go down the recovery * path, we pretend the lookup failed by jumping the JumpMiss branch. * * Takes: * x1 = selector * x16 = class to be searched * * Kills: * x9,x10,x11,x12,x13,x15,x1 * * Untouched: * x14 * * On exit: (found) calls or returns IMP * with x16 = class, x17 = IMP * In LOOKUP mode, the two low bits are set to 0x3 * if we hit a constant cache (used in objc_trace) * (not found) jumps to LCacheMiss * with x15 = class * For constant caches in LOOKUP mode, the low bit * of x16 is set to 0x1 to indicate we had to fallback. * In addition, when LCacheMiss is __objc_msgSend_uncached or * __objc_msgLookup_uncached, 0x2 will be set in x16 * to remember we took the slowpath. * So the two low bits of x16 on exit mean: * 0: dynamic hit * 1: fallback to the parent class, when there is a preoptimized cache * 2: slowpath * 3: preoptimized cache hit * ********************************************************************/源码
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant // // Restart protocol: // // As soon as we're past the LLookupStart\Function label we may have // loaded an invalid cache pointer or mask. // // When task_restartable_ranges_synchronize() is called, // (or when a signal hits us) before we're past LLookupEnd\Function, // then our PC will be reset to LLookupRecover\Function which forcefully // jumps to the cache-miss codepath which have the following // requirements: // // GETIMP: // The cache-miss is just returning NULL (setting x0 to 0) // // NORMAL and LOOKUP: // - x0 contains the receiver // - x1 contains the selector // - x16 contains the isa // - other registers are set as per calling conventions //
// 将x16(class)移动到x15 mov x15, x16 // stash the original isa
LLookupStart\Function:...
.endmacro源码较长,我们分部分看
3.4 LLookupStart\Function 开始查找
- 获取当前 bucket_t 和 HashIndex,用来查找缓存
- 这里对源码的
if-else缩进进行调整,方便查看
- 这里对源码的
LLookupStart\Function: // p1 = _cmd, p16 = isa#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS// ARM64 && __LP64__: MacOS 或 模拟器...#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16// ARM64 && __LP64__ // ClassISA + #CACHE(0x10) 存入 p11,即cache_t(p11) ldr p11, [x16, #CACHE] // p11 = mask|buckets #if CONFIG_USE_PREOPT_CACHES // 真机 #if __has_feature(ptrauth_calls) // > X 的机型 // 判断 cache_t(p11) 第0位是否不为0,如果为0,是否有共享缓存(first_shared_cache_sel) // 没缓存:继续走 // 有缓存:跳转 LLookupPreopt\Function tbnz p11, #0, LLookupPreopt\Function // cache_t(p11) & #0x0000ffffffffffff -> p10(buckets) // 取低 48 位 and p10, p11, #0x0000ffffffffffff // p10 = buckets #else and p10, p11, #0x0000fffffffffffe // p10 = buckets tbnz p11, #0, LLookupPreopt\Function #endif
// (_cmd ^ _cmd >> 7) -> p12 eor p12, p1, p1, LSR #7 // ((cache_t & p12) >> 48) -> p12(HashIndex) and p12, p12, p11, LSR #48 // x12 = (_cmd(p1) ^ (_cmd(p1) >> 7)) & mask #else and p10, p11, #0x0000ffffffffffff // p10 = buckets and p12, p1, p11, LSR #48 // x12 = _cmd & mask #endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4// ARM64 && !__LP64__:真机略#else#error Unsupported cache mask storage for ARM64.#endif // ((buckets + HashIndex) << 1+PTRSHIFT) -> p13(bucket_t) // 通过偏移找到 bucket_t 存入p13 // p12(HashIndex) // `buckets`是`buckets`的首地址,HashIndex为Hash表的下标 // 如:HashIndex为1那就偏移1个`bucket_t`的长度,即:16(sel8+imp8) // 左移`(1+PTRSHIFT))`是为了到`HashIndex-1`位置,这里和cache_t插入缓存的逻辑保持一致,否则找不到的。 // 此时p13存的的是目标bucket_t地址 add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))// #define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// 遍历缓存
// 第一次循环结束后3.5 循环遍历(上)
// do {// #define BUCKET_SIZE (2 * __SIZEOF_POINTER__) 即 (sel + imp 指针的大小)// 取出x13 放入 p17(imp) p9(sel)// 然后 `*bucket--` 向前移动2个指针的长度// 即:一个bucket_t的长度// *bucket--: 先取值,再--1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- // 对比 p1(传入的sel) 和 p9(p13的sel) cmp p9, p1 // if (sel != _cmd) { b.ne 3f // scan more // } else {// 命中调用 CacheHit \Mode2: CacheHit \Mode // hit: call or return imp // }3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; cmp p13, p10 // } while (bucket >= buckets) // 未命中 回到 1 继续循环 b.hs 1b3.6 CacheHit
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa.macro CacheHit.if $0 == NORMAL // 调用缓存IMP TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp.elseif $0 == GETIMP mov p0, p17 cbz p0, 9f // don't ptrauth a nil imp AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP9: ret // return IMP.elseif $0 == LOOKUP // No nil check for ptrauth: the caller would crash anyway when they // jump to a nil IMP. We don't care if that jump also fails ptrauth. AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP cmp x16, x15 cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class) ret // return imp via x17.else.abort oops.endif.endmacro3.7 TailCallCachedImp 执行缓存IMP
#if __has_feature(ptrauth_calls)// JOP...
.macro TailCallCachedImp // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa // (addressOfCachedIMP ^ SEL) -> addressOfCachedIMP eor $1, $1, $2 // mix SEL into ptrauth modifier // (addressOfCachedIMP ^ isa) -> addressOfCachedIMP eor $1, $1, $3 // mix isa into ptrauth modifier // 这个指令还没搞明白 brab $0, $1.endmacro
// JOP#else// not JOP...
.macro TailCallCachedImp // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa // (cachedIMP ^ isa) -> cachedIMP eor $0, $0, $3 // 跳转执行 cachedIMP br $0.endmacro
...
// not JOP#endif3.8 循环遍历(下)
- 当循环回到第一个bucket的时候就停止。
- 将p13移到 buckets + (mask << 1+PTRSHIFT)
- 继续遍历,直到遍历完
- 如果仍然没有命中,_objc_msgSend_uncached
// 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.
... add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) // p13 = buckets + (mask << 1+PTRSHIFT) // see comment about maskZeroBits...
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 4b3.9 LLookupPreopt\Function: 查找共享缓存内的数据
#if CONFIG_USE_PREOPT_CACHES... LLookupPreopt\Function: #if __has_feature(ptrauth_calls) // cache_t(p11) & #0x007ffffffffffffe 存入 p10 and p10, p11, #0x007ffffffffffffe // p10 = buckets // 验证?没找到这个指令的具体作用 autdb x10, x16 // auth as early as possible #endif // 查找共享缓存内的数据 // x12 = (_cmd - first_shared_cache_sel) adrp x9, _MagicSelRef@PAGE ldr p9, [x9, _MagicSelRef@PAGEOFF] sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask) #if __has_feature(ptrauth_calls) // bits 63..60 of x11 are the number of bits in hash_mask // bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...) lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits mov x11, #0x7fff lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits) and x9, x9, x11 // &= mask #else // bits 63..53 of x11 is hash_mask // bits 52..48 of x11 is hash_shift lsr x17, x11, #48 // w17 = (hash_shift, hash_mask) lsr w9, w12, w17 // >>= shift and x9, x9, x11, LSR #53 // &= mask #endif
ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32) cmp x12, w17, uxtw
.if \Mode == GETIMP ... .else b.ne 5f // cache miss sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL br x17 .elseif \Mode == LOOKUP ...
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset add x16, x16, x9 // compute the fallback isa b LLookupStart\Function // lookup again with a new isa .endif#endif // CONFIG_USE_PREOPT_CACHES3.10 GetTaggedClass
- if SUPPORT_TAGGED_POINTERS
// Look up the class for a tagged pointer in x0, placing it in x16..macro GetTaggedClass
and x10, x0, #0x7 // x10 = small tag asr x11, x0, #55 // x11 = large tag with 1s filling the top (because bit 63 is 1 on a tagged pointer) cmp x10, #7 // tag == 7? csel x12, x11, x10, eq // x12 = index in tagged pointer classes array, negative for extended tags. // The extended tag array is placed immediately before the basic tag array // so this looks into the right place either way. The sign extension done // by the asr instruction produces the value extended_tag - 256, which produces // the correct index in the extended tagged pointer classes array.
// x16 = _objc_debug_taggedpointer_classes[x12] adrp x10, _objc_debug_taggedpointer_classes@PAGE add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF ldr x16, [x10, x12, LSL #3]
.endmacro3.11 LNilOrTagged
#if SUPPORT_TAGGED_POINTERSLNilOrTagged:b.eq LReturnZero // nil checkGetTaggedClassb LGetIsaDone// SUPPORT_TAGGED_POINTERS#endif3.12 LReturnZero
LReturnZero:// x0 is already zeromov x1, #0movi d0, #0movi d1, #0movi d2, #0movi d3, #0ret☕️ 看到这里,再休息一下吧。真机汇编看着更带劲儿~
四、真机汇编调试
个人觉得看符号断点的汇编更直观,但是源码中有注释。
真机符号断点的作为参考,和源码对照着再看一遍会更加清晰。
libobjc.A.dylib`objc_msgSend:/*** Start判断接收者是否为空***/-> 0x197b05140 <+0>: cmp x0, #0x0 ; =0x0 0x197b05144 <+4>: b.le 0x197b051e8 ; <+168>/***************************/
/*** Start将接收者存入 x13即:x13 = isa***/ 0x197b05148 <+8>: ldr x13, [x0]/***************************/
/*** StartGetClassFromIsa_p16 ->ExtractISA:isa & ISA_MASK -> x16即:x16 = class的isa***/ 0x197b0514c <+12>: and x16, x13, #0xffffffff8/***************************/
/*** StartCacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached查找缓存***/ 0x197b05150 <+16>: mov x15, x16 // LGetIsaDone
/*** Start LLookupStart\Function: ClassISA + #CACHE(0x10) 存入 p11,即cache_t(p11) 判断 cache_t 第0位是否不为0,如果为0,是否有共享缓存。有就跳转 LLookupPreopt ***/ 0x197b05154 <+20>: ldr x11, [x16, #0x10] 0x197b05158 <+24>: and x10, x11, #0xfffffffffffe 0x197b0515c <+28>: tbnz w11, #0x0, 0x197b051b0 ; <+112>
/*** Start 计算 cache_hash (sel ^ sel >> 7) -> p12 ((cache_t & p12) >> 48) -> p12(HashIndex) ***/ 0x197b05160 <+32>: eor x12, x1, x1, lsr #7 0x197b05164 <+36>: and x12, x12, x11, lsr #48 /***************************/
/***************************/
/*** Start 通过偏移找到 bucket_t 存入x13 x12(HashIndex) `buckets`是`buckets`的首地址,HashIndex为Hash表的下标 如:HashIndex为1那就偏移1个`bucket_t`的长度,即:16(sel8+imp8) 左移`(1+PTRSHIFT))`是为了到`HashIndex-1`位置,这里和cache_t插入缓存的逻辑保持一致,否则找不到的。 (buckets + HashIndex << 1+PTRSHIFT) -> p13(bucket_t) 此时x13存的的是目标bucket_t地址 ***/ 0x197b05168 <+40>: add x13, x10, x12, lsl #4 /***************************/
/*** Start 遍历查缓存 #define BUCKET_SIZE (2 * __SIZEOF_POINTER__) 即 (sel + imp 指针的大小) 取出 x13 - 0x10 放入 p17(imp) p9(sel) 0x10 = 2,即2个单位 `*bucket--` 向前移动2个指针的长度(sel + imp) 即:前移一个bucket_t的长度,将x13指向上一个缓存 对比缓存和_cmd ***/ 0x197b0516c <+44>: ldp x17, x9, [x13], #-0x10 0x197b05170 <+48>: cmp x9, x1 0x197b05174 <+52>: b.ne 0x197b05180 ; <+64>
/*** Start 命中缓存 TailCallCachedImp (cachedIMP ^ isa) -> cachedIMP ***/ 0x197b05178 <+56>: eor x17, x17, x16 0x197b0517c <+60>: br x17 /***************************/
/*** Start while 循环条件判断 if (sel(x9) == 0) goto Miss; while (bucket >= buckets) 满足条件回到 +44 指令行继续循环 ***/ 0x197b05180 <+64>: cbz x9, 0x197b054c0 ; _objc_msgSend_uncached 0x197b05184 <+68>: cmp x13, x10// 0x197b05188 <+72>: b.hs 0x197b0516c ; <+44> /***************************/
/*** Start 当循环回到第一个bucket的时候就停止循环后:
* 将p13移到 buckets + (mask << 1+PTRSHIFT) * 继续遍历,直到遍历完 * 如果仍然没有命中,_objc_msgSend_uncached ***/ 0x197b0518c <+76>: add x13, x10, x11, lsr #44 0x197b05190 <+80>: add x12, x10, x12, lsl #4
/*** Start 第二次遍历 ***/ 0x197b05194 <+84>: ldp x17, x9, [x13], #-0x10
/*** Start 命中缓存 跳转 56指令行:TailCallCachedImp ***/ 0x197b05198 <+88>: cmp x9, x1 0x197b0519c <+92>: b.eq 0x197b05178 ; <+56> /***************************/
// 循环条件判断 0x197b051a0 <+96>: cmp x9, #0x0 ; =0x0 0x197b051a4 <+100>: ccmp x13, x12, #0x0, ne // bucket > first_probed) 0x197b051a8 <+104>: b.hi 0x197b05194 ; <+84> /***************************/
0x197b051ac <+108>: b 0x197b054c0 ; _objc_msgSend_uncached /***************************/
/*** Start LLookupPreopt\Function 查找共享缓存内的数据 ***/
// x12 = (_cmd - first_shared_cache_sel) 0x197b051b0 <+112>: adrp x9, 227000 0x197b051b4 <+116>: add x9, x9, #0x47b ; =0x47b 0x197b051b8 <+120>: sub x12, x1, x9
// bits 63..53 of x11 is hash_mask // bits 52..48 of x11 is hash_shift 0x197b051bc <+124>: lsr x17, x11, #48 0x197b051c0 <+128>: lsr w9, w12, w17 0x197b051c4 <+132>: and x9, x9, x11, lsr #53
// x17 == sel_offs | (imp_offs << 32) 0x197b051c8 <+136>: ldr x17, [x10, x9, lsl #3] 0x197b051cc <+140>: cmp x12, w17, uxtw
0x197b051d0 <+144>: b.ne 0x197b051dc ; <+156> 0x197b051d4 <+148>: sub x17, x16, x17, lsr #32
0x197b051d8 <+152>: br x17
/*** Start 5. ***/ // offset -8 is the fallback offse 0x197b051dc <+156>: ldursw x9, [x10, #-0x8] // compute the fallback isa 0x197b051e0 <+160>: add x16, x16, x9 // lookup again with a new isa 0x197b051e4 <+164>: b 0x197b05154 ; <+20> /***************************/
/***************************/
/*** Start 接收者为空时调用 LNilOrTagged ***/ 0x197b051e8 <+168>: b.eq 0x197b0520c ; <+204>
/*** Start GetTaggedClass ***/ 0x197b051ec <+172>: and x10, x0, #0x7 0x197b051f0 <+176>: asr x11, x0, #55 0x197b051f4 <+180>: cmp x10, #0x7 ; =0x7 0x197b051f8 <+184>: csel x12, x11, x10, eq 0x197b051fc <+188>: adrp x10, 255422 0x197b05200 <+192>: add x10, x10, #0xb60 ; =0xb60 0x197b05204 <+196>: ldr x16, [x10, x12, lsl #3] /***************************/
0x197b05208 <+200>: b 0x197b05150 ; <+16> // LGetIsaDone /***************************/
/*** Start LReturnZero ***/ 0x197b0520c <+204>: mov x1, #0x0 0x197b05210 <+208>: movi d0, #0000000000000000 0x197b05214 <+212>: movi d1, #0000000000000000 0x197b05218 <+216>: movi d2, #0000000000000000 0x197b0521c <+220>: movi d3, #0000000000000000 0x197b05220 <+224>: ret /***************************/🎉 感谢能耐心看到这里,不错不错
- 看汇编是真的费劲儿
objc_msgSend 汇编流程图

五、未命中缓存的处理 __objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band r10 is the searched class
// r10 is already the class to searchMethodTableLookup NORMAL // r11 = IMPjmp *%r11 // goto *imp
END_ENTRY __objc_msgSend_uncached这里会调用 MethodTableLookup。
5.1 查找方法列表 MethodTableLookup
.macro MethodTableLookup // 创建一个栈,为调用方法做准备,存储寄存器中的值 SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) // receiver and selector already in x0 and x1 mov x2, x16 mov x3, #3
bl _lookUpImpOrForward
// IMP in x0 mov x17, x0
// 复原 SAVE_REGS用到的寄存器,出栈 RESTORE_REGS MSGSEND
.endmacro5.2 慢速查找 _lookUpImpOrForward
| 参数 | 说明 |
|---|---|
id inst | receiver(x0) |
SEL sel | selector(x1) |
Class cls | ClassISA |
int behavior | method lookup |
behavior:
LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
流程图

源码解析
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){ const IMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil; Class curClass;
runtimeLock.assertUnlocked();
/** 发给Class的第一个消息通常是通过了objc_opt_*或各种优化的入口: + new + alloc + self 但是,此时Class尚未初始化 */ if (slowpath(!cls->isInitialized())) { behavior |= LOOKUP_NOCACHE; }
/** 加锁 保证 method-lookup + cache-fill 的原子性。 否则 可能分类添加后会被永久忽略,因为缓存被旧的数据覆盖 */ runtimeLock.lock();
/** 为防止 CFI攻击 检查是否是已注册的类 确保Class编译进了二进制文件 或者是使用 objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair 合法的注册过的 */ checkIsKnownClass(cls);
// 如果需要的话初始化class cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again runtimeLock.assertLocked(); curClass = cls;
/** 死循环。通过goto和break跳出 */ for (unsigned attempts = unreasonableClassCount();;) { if (curClass->cache.isConstantOptimizedCache(/* strict */true)) { ... } else { // curClass method list. // 用二分法查找sel Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp(false); goto done; }
// 这里将获取了父类并赋值给 curClass if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // 父类都找完了(superClass == nil)也没有就进行消息转发了 // No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; break; } } ...
// Superclass cache. 这里的 curClass 已经是父类了 imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { // 需要进行消息转发 break; } if (fastpath(imp)) { // Found the method in a superclass. Cache it in this class. goto done; } }
...
done: if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {#if CONFIG_USE_PREOPT_CACHES // iOS真机 while (cls->cache.isConstantOptimizedCache(/* strict */true)) { cls = cls->cache.preoptFallbackClass(); }#endif // 插入缓存 log_and_fill_cache(cls, imp, sel, inst, curClass); }...}5.3 二分查找赏析
这里的算法比较精妙,值得学习。
/*********************************************************************** * search_method_list_inline **********************************************************************/template<class getNameFunc>ALWAYS_INLINE static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName){ ASSERT(list);
auto first = list->begin(); auto base = first; decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key; uint32_t count;
/** count >>= 1,右移一位,即二分一次 */ for (count = list->count; count != 0; count >>= 1) { // 居中的下标 probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) { // `probe` is a match. // Rewind looking for the *first* occurrence of this value. // This is required for correct category overrides. /** probe > first 开始遍历一直把同名的遍历完,因为有分类的存在,所以可能会有同名的。 会掉分类的。 */ while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) { probe--; } return &*probe; }
if (keyValue > probeValue) { base = probe + 1; count--; } }
return nil;}六、动态决议与消息转发
附:知识点补充
1. 取 buckets 过程的补充说明
// 判断 cache_t(p11) 第0位是否不为0,如果为0,是否有共享缓存(first_shared_cache_sel)// 没缓存:继续走// 有缓存:跳转 LLookupPreopt\Functiontbnz p11, #0, LLookupPreopt\Function// cache_t(p11) & #0x0000ffffffffffff -> p10(buckets)// 取低 48 位and p10, p11, #0x0000ffffffffffff // p10 = buckets- 这里的第0位,是指低位的0位
2. OBJC2
我们在进行源码学习的时候时常会见到__OBJC2__这个宏定义。
-
因为
runtime有两个版本- 1.0:用于早期系统上,32位的MacOS上
- 2.0(现行版本):
iOS和MacOS X 10.5以后的64位系统上
-
想要了解更多可以查看下面的官方文档: