前言
在之前的OC类的探索(三) - cache_t分析中,我们分析了方法缓存的调用流程,然后经过向上的探索,发现了objc_msgSend,今天来探索一下这个。
一、知识准备
1.资料
objc源码Runtime
2.Runtime
2.1 Runtime简介
- 编译时:顾名思义正在编译的时候,啥叫编译呢?就是编译器把源代码翻译成机器能够识别的代码。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查
- 运行时:代码跑起来,被装装载到内存中。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断
2.2 Runtime版本
- 早期版本对应的编程接口:Objective-C 1.0
- 现行版本对应的编程接口:Objective-C 2.0,
- 源码中经常看到的OBJC2早期版本用于Objective-C 1.0,32位的Mac OS X的平台
- 现行版本用于Objective-C 2.0,iPhone程序和Mac OS X v10.5及以后的系统中的64位程序
2.3 Runtime调用三种方式
- Objective-C方式,[penson sayHello]
- Framework & Serivce方式,isKindOfClass
- Runtime API方式,class_getInstanceSize
- 三者关系图:
3.上次知识点补充
3.1 为什么扩容是在容量的 3/4 时进行?
- 3/4作为负载因子是大多数数据结构算法的共识,负载因子在0.75时空间的利用率是相对较大的;
- cache存入方法时是根据hash算法计算出来的值作为存储下标,缓存空间的剩余大小对下标是否冲突至关重要,当3/4作为负载因子发生hash冲突的几率相对较低;
二、方法的本质objc_msgSend
1.代码查看objc_msgSend
老规矩 直接上代码
MHTeacher *t = [MHTeacher alloc];
[t sayHello];
[t skill:@"1222"];
编译一下
MHTeacher *t = ((MHTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MHTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)t, sel_registerName("sayHello"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)t, sel_registerName("skill:"), (NSString *)&__NSConstantStringImpl__var_folders_jt_46dmknbx2pb23kf9n3jncn740000gn_T_main_5862f5_mi_0);
我们看到 无论是alloc 、sayHello还是skill: 都变成了 objc_msgSend(id,sel_registerName(方法名)) 然后就是参数
所以方法的本质是objc_msgSend 消息转,那让我们来实验一下:
[t sayHello];
objc_msgSend((id)t,sel_registerName("sayHello"));
一样的结果:
111
111
tips
- 必须导入相应的头文件#import <objc/message.h>
- 关闭objc_msgSend检查机制:target --> Build Setting -->搜索objc_msgSend – Enable strict checking of obc_msgSend calls设置为NO
搜一下源码,发现了:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-library-redeclaration"
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#pragma clang diagnostic pop
#else
芜湖还有个objc_msgSendSuper,有一个这个objc_super 看一下
#ifndef OBJC_SUPER
#define 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 */
};
#endif
再调用一下,成功了。
struct objc_super my_objc_super;
my_objc_super.receiver = t;
my_objc_super.super_class = MHPerson.class;
objc_msgSendSuper(&my_objc_super, @selector(sayHello1));
[61947:5799740] 2222
2.汇编查看objc_msgSend
搜的过程中发现了这个:
ENTRY _objc_msgSend // /_ objc_ _msgSend入口,此时有两个参数-个是(就是isa)id receiver 还有一个是SEL_cmd
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check receiver和0比较
#if SUPPORT_TAGGED_POINTERS // __LP64__ 64位系统支持Taggedpointer类型
b.le LNilOrTagged // (MSB tagged pointer looks negative) 小于等于0支持Taggedpointer类型 走LNilOrTagged流程
#else
b.eq LReturnZero // 等于0直接返回nil就是给- -个空对象发消息
#endif // 对象有值或者说isa有值
ldr p13, [x0] // p13 = isa //把x0寄存器里面的地址读取到p13寄存器,对象的地址等于isa的地址
GetClassFromIsa_p16 p13, 1, x0 // p16 = class //
LGetIsaDone: // 这是一 个标记符号,拿到isa操作完以后继续后面的操作
// calls imp or objc_msgSend_uncached 传递三个参数 NORMAL_ objc_ msgSend__ 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
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
ENTRY _objc_msgLookup
2.1大致思路:
判断receiver是否等于nil, 在判断是否支持Taggedpointer小对象类型
- 支持Taggedpointer小对象类型,小对象为空 ,返回nil,不为nil处理isa获取class跳转CacheLookup流程
- 不支持Taggedpointer小对象类型且receiver = nil,跳转LReturnZero流程返回nil
- 不支持Taggedpointer小对象类型且receiver != nil,通过GetClassFromIsa_p16把获取到class 存放在p16的寄存器中,然后走CacheLookup流程,
2.2 GetClassFromIsa_p16
GetClassFromIsa_p16 核心功能获取class存放在p16寄存器
// p13 , 1 , x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, \src // optimistically set dst = src
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__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address // 把 \src \auth_address 传进ExtractISA 得到的结果赋值给p16寄存器
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
2.3 ExtractISA
ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
...
.macro ExtractISA
and $0, $1, #ISA_MASK // and 表示 & 操作, $0 = $1(isa) & ISA_MASK = class
.endmacro
// not JOP
#endif
2.4 CacheLookup流程
// NORMAL, _objc_msgSend, __objc_msgSend_uncached
.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
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets //CACHE 2*8 (2 * __SIZEOF_POINTER__) cache_t
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets //CACHE 2*8 (2 * __SIZEOF_POINTER__) cache_t
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function // 不为0就跳转LLookupPreopt
#endif
eor p12, p1, p1, LSR #7 // p1右移7位
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 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
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
// 源码调试 + 汇编
add p13, p10, p12, LSL #(1+PTRSHIFT) // PTRSHIFT = 3
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
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
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
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
b.ne \MissLabelConstant // cache miss
sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
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_CACHES
.endmacro
源码分析:首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了三件事
- 获取_bucketsAndMaybeMask地址也就是cache的地址:p16 = isa(class),p16 + 0x10 = _bucketsAndMaybeMask = p11
- 获取buckets地址就是缓存内存的首地址:buckets = ((_bucketsAndMaybeMask >> 48 )- 1 )
- 获取hash下标: p12 =(cmd ^ ( _cmd >> 7))& msak 这一步的作用就是获取hash下标index
- 流程如下:isa --> _bucketsAndMaybeMask --> buckets -->hash下标
- 根据下标index 找到index对应的bucket。p13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))
- 先获取对应的bucket然后取出imp和sel存放到p17和p9,然后*bucket–向前移动
- 1流程:p9= sel和 传入的参数_cmd进行比较。如果相等走2流程,如果不相等走3流程
- 2流程:缓存命中直接跳转CacheHit流程
- 3流程:判断sel = 0条件是否成立。如果成立说明buckets里面没有传入的参数_cmd的缓存,没必要往下走直接跳转__objc_msgSend_uncached流程。如果sel != 0说明这个bucket被别的方法占用了。你去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket和第一个bucket地址大小,如果大于第一个bucket的地址跳转1流程循环查找,如果小于等于则接继续后面的流程
- 如果循环到第1个bucket里都没有找到符合的_cmd。那么会接着往下走,因为下标index后面的可能还有bucket还没有查询
- 4流程: 如果 bucket 已经走到了 0 位置,还不相等:
`buckets + (mask << 1+PTRSHIFT) => mask 向左移动了 4 位置 => 7 16 => p13 定位到最后一个的位置。
add p12, p10, p12, LSL #(1+PTRSHIFT) => p12 = first probed bucket
ccmp p13, p12, #0, ne // bucket > first_probed)
bucket > first_probed原因:因为之前已经比较过一次,所以这里就必须大于 first_probed 否则还要走一次进行比较`
2.5 CacheHit
.macro CacheHit
.if $0 == NORMAL
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 IMP
9: 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
.endmacro
- mode 为normal 所以执行TailCallCachedImp
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
.macro TailCallCachedImp
// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
eor $0, $0, $3 // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址
br $0 //调用 imp
.endmacro
...
#endif
- 缓存查询到以后直接对bucket的imp进行解码操作。即imp = imp ^ class,然后调用解码后的imp
三、总结:
语言总是苍白的,所以让我们借用一张图来总结一下objc_msgSend的流程:
请多指教