前言
我们知道OC的上层方法调用时,在底层都会转化为objc_msgSend调用,那么它的流程是怎么样的呢,我们又如何理解它, 我们带着这些疑问来分析objc_msgSend。
首先我们先难证一下,OC的上层方法是不是会转化成objc_msgSend,如下图:
我们定义了一个RoPerson类的对象并调用了saySomething,通过汇编我们可以清楚的看到是转化为了objc_msgSend调用,这也就说明了OC上层的方法会被转化为objc_msgSend调用,并且是在libobjc.A.dylib这个动态库中,objc_msgSend是有汇编写的。
objc_msg_Send汇编分析
我们在objc(818版本)的源码中找到objc_msg_Send的代码,如代码所示:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check,判断当前的消息接受者是否为0,如果没有消息接受者,就无意义了
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa,把当前的isa给到p13寄存器
GetClassFromIsa_p16 p13, 1, x0 // p16 = class,p13就是isa,x0也就isa
LGetIsaDone: // 这里获取isa已经完成,开始执行下一步操作
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
// 这几行代码如果是*SUPPORT_TAGGED_POINTERS*(后续补充)执行*b.le LNilOrTagged*,否则执行*b.eq LReturnZero*。
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
我们接着分析 GetClassFromIsa_p16这个函数,看看他到底做了什么,代码如下:
// p13(isa)也就是src参数, 1是needs_auth参数, x0(isa)是auth_address参数
.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
// 这里不是index isa,所以不执行,直接执行1
1:
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already,needs_auth传过来的是1,所以下面一行代码不执行,执行.else的代码
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address // p16是一个空的的地址,src,auth_address都是isa
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
我们再看下ExtractISA这个函数的流程执行,全局搜下:
// p16 = isa & ISA_MASK
.macro ExtractISA
and $0, $1, #ISA_MASK // 这行代码就是, $1 逻辑与(按位)ISA_MASK,然后给到$0 也就是p16,and是逻辑与,
.endmacro
这也就是解释了p16 就是class。
接着我们再来分析CacheLookup,我们搜下它的宏:
// Mode就是NORMAL, Function就是_objc_msgSend,__objc_msgSend_uncached, MissLabelDynamic , MissLabelConstant
.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,这里x16就是p16,把x16移到x15寄存器中
LLookupStart\Function: // 开始找_objc_msgSend流程
// p1 = SEL, p16 = 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 // 真机的架构。
ldr p11, [x16, #CACHE] // p11 = mask|buckets,把x16的地址平移CACHE大小,经过全局搜索CACHE是16字节,就是平移到cahce结构体的位置,这个时候p11=cache_t。
#if CONFIG_USE_PREOPT_CACHES // 这里从来的没找过,所以不执行,我们可以看下它的else,CONFIG_USE_PREOPT_CACHES这个值为1,可以全局搜下
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets,这里就是p11 & 0x0000fffffffffffe(掩码),并赋给10寄存器
tbnz p11, #0, LLookupPreopt\Function,p11与0做比较,如果p11不为0,跳转到LLookupPreopt。
#endif
eor p12, p1, p1, LSR #7 // p1右移7位存到p12中
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
// p11 cache -> p10 = buckets
// p11, LSR #48 -> mask,
// p1(_cmd) & mask = index -> p12
and p10, p11, #0x 000 0ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask,p11 右移48位得到mask值,然后与p1(sel也就是_cmd)进行&运算,得到index值,并赋给p12寄存器。
#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
// objc - 源码调试 + 汇编
// p11 cache -> p10 = buckets
// p1(_cmd) & mask = index -> p12
// (_cmd & mask) << 4 (左移4位),buckets+就是内存平移,就是平移int(占用4字节)的大小
// buckets + 内存平移 (1 2 3 4)
// b[i] -> b + i
// p13 当前要查找的bucket
// PTRSHIFT=3
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),
// do {
// *bucket-- p17, p9
// bucket 里面的东西 imp (p17) sel (p9)
// 查到的 sel (p9) 和我们(要查的) say1
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--,x13往上移BUCKET_SIZE这个大小,存到p9(sel),p17(imp)这个位置
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more,如果不等于,执行3,继续查找
// } else {
2: CacheHit \Mode // hit: call or return imp,缓存命中(跳转或者返回imp)
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b // 跳转到1,继续执行
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits o n 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
这里我们分析下LLookupPreopt这个函数,搜到如下:
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets, 这里是p11寄存器逻辑与(按位)0x007ffffffffffffe,给到p10,也就是说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
我们再来看下CacheHit这个函数,代码如下:
// 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
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
TailCallCachedImp这个函数,就是对imp进行编码,并跳转到imp
慢速查找流程
慢述查找的基本流程是:
1 查自己的methodlist,如果查找到sel和imp,返回
2 如果没查找到,去查找父类的methoslist,如果查找到sel和imp,返回
3 接着去查找NSObject,如果还是没有查到,就跳转出,下面我们来验证一下。
慢速查找流程准备
我们在objc的源码中搜下lookUpImpOrForward这个C++函数,我们把代码贴出来分析下:
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;
我们对这个源码分析一下
checkIsKnownClass(cls);检查当前的class是否已经注册到当前的缓存中。
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);对rw,ro的一些处理以及isa走位。
二分查找
上述的代码片段中
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
这是从共享缓存中查找,我们在处理rw,ro,methodslist的时候,这个时候有可能正好写入了方法,所以我们再查找一遍。
接下来,我们来看下**getMethodNoSuper_nolock这个函数的实现流程:
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
我们再来看下search_method_list_inline这个函数:
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->isExpectedSize();
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name() == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
endif
return nil;
}
我们再来看下***findMethodInSortedMethodList*这个函数是从排好序的搜索方法list:
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
if (list->isSmallList()) {
if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
} else {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
}
} else {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
}
}
接着我们来分析**findMethodInSortedMethodList**函数的代码:
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;
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.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
for循环中的count >>= 1的运算就是 就是取count的中间值,也就是平均值,即二分。
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
这里就是主类的方法和分类的方法同名时,分类重写,也就是分类优先。
如果查找到的话,直接goto 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);
}
log_and_fill_cache 这里进行缓存填充,下次就不再进行二分查找,也就是在第一次查找的时候会进行慢速查找并插入到缓存中,下次就不再进行慢速查找。
如果没有查找到方法的话,会执行下面代码:
// 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;
}
通过父类去查找,父类同样的先进行快速查找(汇编快速查找),如果没有找到再慢速查找,如果还是没找到,再拿到父类进行快速和慢速的查找,这就是递归的过程。
以上就是OC底层慢速查找流程。
动态方法决议
对象方法动态决议
首先我们来看下这张图:
从这张图我们都可以看出我们的方法未找到而报错,那么它的原理是什么,又做了什么操作呢,让我们一步一步往下分析。
首先我们在lookUpImpOrForward函数中第一行代码
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
这里有一个默认的赋值imp。
而这行代码
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
如果我们没有找到imp的话,会把默认的forward_imp给到imp,然后返回,通过一系列的分析(中间有汇编代码,我们这里不做介绍了),发现以下代码:
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
上面那张图就是这里打印出来的。
当我们的方法找不到的时候,会有一个过程,我们来分析一下。
首先我们断点一下,如图:
然后我们找到lookUpImpOrForward这个函数
断点调试,如图:
然后再进入这个行代码,如图:
然后我们看下imp对象是什么,如图:
这时候imp获取不到,这个时候会进入6500行这代码的流程,这个是单例,只会进入一次,这里的behavior通过查找是3,这里通过&运算后,就不会再进来了。
而这里会进入resolveMethod_locked这个函数,代码如下:
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
如果我们的方法找不到的话,在这里我们还有一次机会进行拯救一下后返回imp,lookUpImpOrForwardTryCache这个函数会重新进行查找一次返回imp。那么是如何重新处理这个方法找不到的问题呢,我们接着分析。
我们来看下以下代码:
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
这里判断cls(当前对象的类)如果不是元类,调起resolveInstanceMethod这个函数,如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
这里系统会自动发消息
BOOL (msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);*
如果在类里面resolve_sel有这个消息并且处理的话,就会调用IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);这行代码继续查找一遍,resolve_sel这个对象就是** resolveInstanceMethod这个,也就是说只要实现resolveInstanceMethod**这个方法,就可以进行容错处理,因为这是往cls(类)发送消息,也就是类方法。
然后我们在RoTeacher的m文件加入以下代码,如图:
我们来运行一下,如图:
然后我们在RoTeacher的m文件加入以下代码,如图:
进着我们再运行一下,看结果如何,如图所示:
我们的程序不再闪退了,可以正常运行了,这说明我们只要重新处理resolveInstanceMethod这个函数,就可以使程序更健壮一些,这就是对象方法的决议
类方法的动态决议
我们先看以下代码:
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
这个是类方法动态决议的关键代码,我们接着分析,我们先断点调试,如图:
以下代码是类方法动态决议的代码:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);这行代码的nonmeta是要写在元类里,而resolveClassMethod是一个类方法,类的类方法在元类里存在,所以resolveClassMethod这个方法写在RoTeacher类里。我们在RoTeacher中加入以下代码:
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod :%@-----%@", self, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
然后运行,如图所示:
然后,我们再加入以下代码:
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(sayHappy)) {
IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("RoTeacher"), @selector(sayRo));
Method method = class_getInstanceMethod(self, @selector(sayRo));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("RoTeacher"), sel, sayNBImp, type);
}
NSLog(@"resolveClassMethod :%@-----%@", self, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
接着我们运行看下结果:
这说明我们实现成功了,没有闪退,这说明类方法的动态决议,当然我们可以建一个NSObject分类统一处理,这里不再做介绍。
消息转发流程
快速转发流程
如果我们没有在方法动态决议这一层处理的话,程序依然会报错,那么我们还有没有机会处理呢,我们带着这个疑问往下继续探索。
我们在lookUpImpOrForward这个函数中没有找到转发流程的相关代码,那么该怎么办?
我们先来介绍下instrumentObjcMessageSends这个函数底层调用的预知打印,我们来试下,我们在objc的源码中搜索下instrumentObjcMessageSends这个函数,如下代码:
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
经过一系列的分析,我们有看到系统会写入日志文件,我们打开这个文件(/tmp/msgSend目录),找到了** forwardingTargetForSelector**这个方法,我们继续分析这个。
我们在官方文档下搜了下这个方法,如图:
这个方法的意思是返回一个对象成为当前没有被识别的消息的继承者,也就是重定向,这个是快速转发的执行者。
我们在RoTeacher的m的文件中加入以下代码:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%S - %@", __func__, NSStringFromSelector(@selector(aSelector)));
return [super forwardingTargetForSelector:aSelector];
}
在头文件加入:
- (void)sayHello;
我们不在m文件中实现。
接着我们运行项目,如图:
我们通过打印日志可以看出我们的forwardingTargetForSelector接收到了,
我们先在RoPerson加入sayHello的实现,我们修改forwardingTargetForSelector
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s ------------- %@", __func__, NSStringFromSelector(aSelector));
return [RoPerson alloc];
}
然后运行,如图:
我们看到这里没有闪退,说明成功转发到一个局部对象上了,这就是快速转发流程。
慢速转发流程
如果RoPerson没有实现sayHello呢,在实际项目中,我们无法确定某一个方法一定会存在,这个时候我们该怎么处理呢,我们接着分析。
我们在官方文档搜索下** methodSignatureForSelector**这个方法,如图:
这个方法是返回当前方法的签名。
接着我们在RoTeacher加入以下代码:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s ------------- %@", __func__, NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
从这张图中可以看出没进入慢速转发流程,methodSignatureForSelector没调用,在快速转发流程中,我们转发到了RoPerson,它会进行一系列的,快速查,慢速查找,动态方法决议流程,如果都没有实现,RoPerson的方法,但是RoPerson没有实现sayHello,所以没走到methodSignatureForSelector这个方法,我们注释掉快速转发流程,然后再运行,如图:
这样进入了慢速转发的流程,但是这里并没有解读成功,是因为这个方法需要另一个方法配合使用。
接着我们再修改RoTeacher的m文件,代码如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s ------------- %@", __func__, NSStringFromSelector(aSelector));
if (aSelector == @selector(sayHello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
}
接着运行,如图:
这样没有闪退了,但是没做任何操作。
接着我们修改forwardInvocation代码如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
RoPerson *r = [RoPerson alloc];
if ([self respondsToSelector:anInvocation.selector]) {
[anInvocation invoke];
}
else if([r respondsToSelector:anInvocation.selector] ) {
[anInvocation invokeWithTarget:r];
}
else {
NSLog(@"这里有闪退问题");
}
NSLog(@"%@----%@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
再次运行,如图:
这里一直存着方法。这就是慢速转发流程,相对于快速转发流程给我们更加自由灵活的处理。
结语
以上就是个人对objc_msgSend的汇编,快速查找,慢速查找,动态决议,快速转发,慢速转发的一些见解,还有许多不完善的地方,请大家多指教。
补充 动态方法决议走两次的原因
我们先看张图,如下:
say666方法不存在,resolveInstanceMethod方法调用了两次,这是为什么,我们来分析下。
我们在resolveInstanceMethod打个断点调试,如图:
接着我们运行程序,我们在第二次打印之前,bt看下堆栈,如图:
我们在frame#12中看到_CF_forwarding_prep_0这个函数,这一步来自于CF调起了_CF_forwarding_prep_0,这里做了相关的操作,这又是为什么?我们接着往下看。
在这个过程中,我们做了消息转发的流程,我们的消息转发流程也是对程序做了容错处理。
通过搜索这个方法class_respondsToSelector_inst,发现里面又调了lookUpImpOrNilTryCache函数,最终调了lookUpImpOrForward函数,又再给一次机会进行补救,如果还是找不到方法,再进行动态方法决议,这也是为什么动态方法决议调起两次的原因。
我们再来看下面一张图:
从张图中,可以看出,第二次的say666的打印,是在快速和慢速转发流程后或者由它发起的调用,不是无限递归,也就是系统内部函数做了一个流程。