前言
前面我探究了cache_t和方法的缓存机制,接下来我们理应探究方法的查找了,但是在探究方法查找之前我们需要明白rutime
和方法的本质
。
一、runtime
什么是runtime
?简单的说就是运行时。还有一种编译时
。
编译时
就是简单的作⼀些翻译⼯作 ,⽐如检查⽼兄你有没有粗⼼写错啥关键字了啊.有啥词法分析,语法分析之类的过程. 就像个⽼师检查学⽣的作⽂中有没有错别字和病句⼀样 .如果发现啥错误编译器就告诉你.如果你⽤微软的VS的话,点下build.那就开始编译,如果下⾯有errors或者warning信息,那都是编译器检查出来的.所谓这时的错误就叫编译时错误,这个过程中做的啥类型检查也就叫编译时类型检查,或静态类型检查运行时
就是代码跑起来了,被装载到内存种了,⽽运⾏时
类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做些操作,做些判断.
是由汇编
、c
、c++
构成的一套为Objective-C
提供运行时的API
。
1、版本
Runtime
有两个版本 ⼀个Legacy
版本(早期版本) ,⼀个Modern
版本(现⾏版本)。
- 早期版本对应的编程接⼝:
Objective-C 1.0
,用于32位的Mac OS X的平台上; - 现⾏版本对应的编程接⼝:
Objective-C 2.0
,iPhone程序和Mac OS X v10.5 及以后的系统中的 64 位程序。
2、调起方式
runtime有3种调起方式:
OC
层面:[[NSObkect alloc] init]
;NSObject
类中定义的方法:performSelector:withObject:
;Runtime API
:objc_msgSend(...);
二、方法本质
我们先创建一个类,并添加方法,如图:
然后clang:clang -rewrite-objc main.m -o main.cpp
打开生成的main.cpp
并找到main
函数入口,如图:
稍微简化一下,如图:
不难发现,所谓的方法在底层就是objc_msgSend
消息的发送。参数为消息接收者p
和sel
及参数。
结论:方法的本质就是objc_msgSend
。
三、objc_msgSend
我们已经发现方法的本质就是objc_msgSend
,现在我们来探究一下。
1、源码分析
首先我们进入objc_msgSend
,如图:
到这里我们就发现无法进入实现,这里就要想一下:runtime
是由汇编
、c
、c++
构成的一套为Objective-C
提供运行时的API
。 所以我们可以找汇编的实现,如图:
23个文件中645个结果,有点吓人。23个文件中645个结果,有点吓人。但是记住一点 我们找的是汇编,是.s
文件,简单处理一下,如图:
这些.s
文件都是objc_msgSend
的实现,根据cpu架构的不同汇编的语法也有所不同,但是实现的过程基本都是一样的,我们选择arm64
的进行分析,如图:
打开之后发现也还是好多,此时找到ENTRY _objc_msgSend
,如图:
大致流程:
- 判断:
1.1 接收者是否为空;
1.2 判断小对象,如果小对象为空,则直接返回空,即LReturnZero;是小对象执行LNilOrTagged;
1.3 LNilOrTagged再次判断接收者是否为空,空则直接返回空,即LReturnZero;非空则GetTaggedClass,然后跳转到LGetIsaDone;
1.4 既不是小对象,接受者也非空就会GetClassFromIsa_p16,获取class并存在p16
复制代码
GetClassFromIsa_p16
,如图:
稍微的对注释进行了翻译。
-
LGetIsaDone
这里有一个宏定义方法:
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
分为: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
//一旦我们超过了LLookupStart\Function标签,我们就可能加载了一个无效的缓存指针或掩码。
//当task_restartable_ranges_synchronize()被调用时,
//(或者当一个信号击中我们)在我们经过LLookupEnd\Function之前,
//然后我们的PC将被重置为LLookupRecover\Function,
//它会强制跳转到cache-miss代码路径,代码路径有以下要求:
//GETIMP:
//缓存未命中只是返回NULL(将x0设置为0)
//正常和查找:
//- x0表示接收端
//-x1包含选择器
//- x16包含isa
//-其他寄存器按照调用约定设置
mov x15, x16 // stash the original isa 将x16的值(isa),赋值给x15
复制代码
缓存查找(快速查找)
LLookupStart\Function:
// p1 = SEL, p16 = isa => isa -> 类的 isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
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
// x16(isa) 平移 16 就得到了 cache的地址(即为_bucketsAndMaybeMask的地址)
// 将 _bucketsAndMaybeMask的地址存入 p11 寄存器中。
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#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
// 如果_bucketsAndMaybeMask第 0 位不等于 0,就跳转到 LLookupPreopt\Function
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// 通过哈希求 index 下标 赋给p12
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)
// 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 不匹配跳转到3
// } else {
2: CacheHit \Mode // hit: call or return imp 缓存命中
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;如果sel==0 没有命中,跳转到 MissLabelDynamic
cmp p13, p10 // } while (bucket >= buckets) 判断是否越界了
b.hs 1b //再哈希跳转到1
// wrap-around:
// p10 = first bucket 第一个桶子
// p11 = mask (and maybe other bits on LP64) 掩码
// p12 = _cmd & mask 通过哈希求 index 下标 赋给p12
//
// 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.
//
//完全缓存可以通过CACHE_ALLOW_FULL_UTILIZATION实现。
//当我们绕回第一个探测的桶时停止
//而不是再次击中第一个桶。
// 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 //IMP returned in x17 (imp 返回在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
复制代码
命中缓存CacheHit
:
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.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
// authenticate imp and re-sign as IMP 检查并重签imp
AuthAndResignAsIMP x0, x10, x1, x16
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
复制代码
AuthAndResignAsIMP
:
.macro AuthAndResignAsIMP
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// note: assumes the imp is not nil 假设imp不为空
eor $1, $1, $2 // mix SEL into ptrauth modifier
eor $1, $1, $3 // mix isa into ptrauth modifier
autib $0, $1 // authenticate cached imp 检查缓存imp
ldr xzr, [$0] // crash if authentication failed 检查失败
paciza $0 // resign cached imp as IMP 重签imp
.endmacro
复制代码
慢速查找__objc_msgSend_uncached
:
在这里调用MethodTableLookup
,内容如下:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1 接收者和sel在x0和x1中
mov x2, x16
mov x3, #3
//跳转到 _lookUpImpOrForward
bl _lookUpImpOrForward
// IMP in x0 找imp 在x0中
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
复制代码
在当前文件中找不到_lookUpImpOrForward
,全局搜索,在objc-runtime-new.mm
中找到了实现,接下来我们又回到了c++
中开始慢速查找,lookUpImpOrForward
的实现如下:
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();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
复制代码
未完待续!