一、汇编缓存找不到
在《OC底层探究之从cache到objc_msgSend》文章中,我们把查找缓存的汇编流程过了一遍,汇编流程就是快速查找
流程,但是还没有进入MissLabelDynamic
流程,这就是慢速查找
流程!
而MissLabelDynamic
就是__objc_msgSend_uncached
:
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
复制代码
发现就执行了2
个函数,先看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
RESTORE_REGS MSGSEND
.endmacro
复制代码
这里就是存储了一些值以及调用了_lookUpImpOrForward
。
在看TailCallFunctionPointer
:
#if __has_feature(ptrauth_calls)
.macro TailCallFunctionPointer
// $0 = function pointer value
braaz $0
.endmacro
#else
.macro TailCallMethodListImp
// $0 = method list imp, $1 = address of method list imp
br $0
.endmacro
#endif
复制代码
发现这里直接就返回了$0
,而$0
就是传入的x17
。
那么x17
是什么呢?
在MethodTableLookup
里有:
// IMP in x0
mov x17, x0
复制代码
即x17
就x0
,x0
就是传入的第一个参数
或者是返回的结果
。
很明显这里的x0
就是_lookUpImpOrForward
返回的结果!
二、慢速查找流程的准备
1、总览lookUpImpOrForward
NEVER_INLINE
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;
}
复制代码
代码很多,但是我们到这个方法的主要目的是找imp
,看return
也可以知道是imp
。
imp
大多都是for (unsigned attempts = unreasonableClassCount();;)
这个循环
内,在因此可得知这个循环
是重点
!
2、判断初始化
从头开始看,一进来先判断了是否初始化
:
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
复制代码
3、检测类
接着调用checkIsKnownClass
,检测当前的类是否已经注册
到当前的缓存表里面:
/***********************************************************************
* checkIsKnownClass
* Checks the given class against the list of all known classes. Dies
* with a fatal error if the class is not known.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
ALWAYS_INLINE
static void
checkIsKnownClass(Class cls)
{
if (slowpath(!isKnownClass(cls))) {
_objc_fatal("Attempt to use unknown class %p.", cls);
}
}
复制代码
通过中间方法,进入isKnownClass
:
/***********************************************************************
* isKnownClass
* Return true if the class is known to the runtime (located within the
* shared cache, within the data segment of a loaded image, or has been
* allocated with obj_allocateClassPair).
*
* The result of this operation is cached on the class in a "witness"
* value that is cheaply checked in the fastpath.
**********************************************************************/
ALWAYS_INLINE
static bool
isKnownClass(Class cls)
{
if (fastpath(objc::dataSegmentsRanges.contains(cls->data()->witness, (uintptr_t)cls))) {
return true;
}
auto &set = objc::allocatedClasses.get();
return set.find(cls) != set.end() || dataSegmentsContain(cls);
}
复制代码
在allocatedClasses
方法获取表:
/***********************************************************************
* allocatedClasses
* A table of all classes (and metaclasses) which have been allocated
* with objc_allocateClassPair.
**********************************************************************/
namespace objc {
static ExplicitInitDenseSet<Class> allocatedClasses;
}
复制代码
4、类的实现
接着到类的实现
方法:
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
复制代码
进过几次中间方法,最终进入到realizeClassWithoutSwift
方法:
/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
runtimeLock.assertLocked();
class_rw_t *rw;
Class supercls;
Class metacls;
if (!cls) return nil;
if (cls->isRealized()) {
validateAlreadyRealizedClass(cls);
return cls;
}
ASSERT(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}
cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
#if FAST_CACHE_META
if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif
// Choose an index for this class.
// Sets cls->instancesRequireRawIsa if indexes no more indexes are available
cls->chooseClassArrayIndex();
if (PrintConnecting) {
_objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
cls->nameForLogging(), isMeta ? " (meta)" : "",
(void*)cls, ro, cls->classArrayIndex(),
cls->isSwiftStable() ? "(swift)" : "",
cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
}
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// This assumes that none of those classes have Swift contents,
// or that Swift's initializers have already been called.
// fixme that assumption will be wrong if we add support
// for ObjC subclasses of Swift classes.
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
#if SUPPORT_NONPOINTER_ISA
if (isMeta) {
// Metaclasses do not need any features from non pointer ISA
// This allows for a faspath for classes in objc_retain/objc_release.
cls->setInstancesRequireRawIsa();
} else {
// Disable non-pointer isa for some classes and/or platforms.
// Set instancesRequireRawIsa.
bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;
if (DisableNonpointerIsa) {
// Non-pointer isa disabled by environment or app SDK version
instancesRequireRawIsa = true;
}
else if (!hackedDispatch && 0 == strcmp(ro->getName(), "OS_object"))
{
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
else if (supercls && supercls->getSuperclass() &&
supercls->instancesRequireRawIsa())
{
// This is also propagated by addSubclass()
// but nonpointer isa setup needs it earlier.
// Special case: instancesRequireRawIsa does not propagate
// from root class to root metaclass
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
}
if (instancesRequireRawIsa) {
cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
}
}
// SUPPORT_NONPOINTER_ISA
#endif
// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}
// Propagate the associated objects forbidden flag from ro or from
// the superclass.
if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
(supercls && supercls->forbidsAssociatedObjects()))
{
rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
}
// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// Attach categories
methodizeClass(cls, previously);
return cls;
}
复制代码
先是rw
、ro
赋值,即属性列表、方法列表等等。
然后实现元类和父类
:
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
复制代码
这里是一个递归
调用。
接着把元类和父类
赋值给当前类
:
// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
复制代码
就是在这样的递归
调用中把isa
走位图的所有相关类都实现了!
为什么要把所有的相关类都实现呢?
因为要找方法
!
对象方法
要是当前类找不到方法就需要去父类中寻找!
类方法
要是当前类的isa找不到方法就需要去父类的isa中寻找!
以上就是为了for循环
做的准备!
三、二分查找流程
按照我们一般的理解,查找对象方法:
1、在自己这里查找,即在methodlist
里面查找sel
和imp
。
2、找不到则去父类
找,再去NSObject
里面去找,最后到nil
报错。
接着我们就来看看是不是这样呢?
进入到for (unsigned attempts = unreasonableClassCount();;)
循环,发现这是一个死循环!
即只有break
或者goto
等才能跳出循环!
进来的第一个判断:
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
:
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;
}
}
复制代码
首先调用getMethodNoSuper_nolock
获取方法列表:
/***********************************************************************
* getMethodNoSuper_nolock
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
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;
}
复制代码
先通过cls->data()->methods()
获取方法列表
,在《OC底层探究之类的底层原理结构》文章中进行过探索。
然后遍历方法列表调用search_method_list_inline
获取方法method_t
:
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
通过排好序的方法列表进行查找:
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; });
}
}
复制代码
这里是判断是SmallList
还是BigList
。
BigList
是传统的电脑,所以先看else
,进入findMethodInSortedMethodList
:
/***********************************************************************
* 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;
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
循环,每次循环完成后,右移1
位,其实就除2
!
然后看循环内:
先是probe
赋值为base + (count >> 1)
,base
是开始位置,即为0
,所以这里probe
为count / 2
。
然后取probe
处的名字即sel
,然后让需要查找的sel
和当前sel
对比,如果一样且probe
大于起始位置
,则让需要查找的sel
继续比较下一个sel
,如果还一样,则probe--
,最后返回probe的地址
!
为什么要比较下一个名字呢?因为可能存在对象方法
和类方法
名字一样的时候!
如果不一致,则对比大小,如果要查找的sel
比当前的sel
大,则说明要查找的sel
在后面,否则就在前面!
在后面的话base
赋值为probe + 1
,同时count--
,最后继续循环。
四、慢速查找递归流程
1、查到方法
当在二分法中找到method_t(方法)
后,直接跳转到done
:
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
:
/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled.
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver);
}
复制代码
最后进入到我们熟悉的insert
函数!
2、去父类中查找
如果在方法列表中没有找到方法,先是判断if (slowpath((curClass = curClass->getSuperclass()) == nil))
,即当前的class
赋值为superclass
且为nil
的时候,break
出去!什么时候会是nil
?NSObject
的父类就是nil
!说明到这里已经把父类全部查完了,都没有查到!然后就会进入消息转发流程
!
如果还有父类,则开始去父类中查找:
// Superclass cache.
imp = cache_getImp(curClass, sel);
复制代码
进入到cache_getImp
:
// returns:
// - the cached IMP when one is found
// - nil if there's no cached value and the cache is dynamic
// - `value_on_constant_cache_miss` if there's no cached value and the cache is preoptimized
extern "C" IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil);
复制代码
发现没有后续了!
全局搜索cache_getImp
:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
复制代码
发现这就是我们之前的汇编查找缓存的流程,即快速查找流程
!
说明当前类找不到方法后,就去父类中进行快速查找流程,查找缓存中的方法!
如果在父类中找到了方法,进入done
,进行缓存。
如果在父类中没有找到方法,继续循环,进行慢速查找流程!
3、方法决议
如果跳出循环,则进入到方法决议resolveMethod_locked!