当快速查找流程找不到方法时候, 走goto Miss
方法
__objc_msgSend_uncached
// 汇编
STATIC_ENTRY __objc_msgSend_uncached // 进入__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 // 返回函数指针
END_ENTRY __objc_msgSend_uncached
- 先看下
TailCallFunctionPointer
方法
.macro TailCallFunctionPointer
// $0 = function pointer value 函数指针
br $0
.endmacro
可看出, 这里实际上是调用函数指针方法, 说明如果走到这里, 函数的imp
已经找到。
那么 MethodTableLookup
这个方法即是找到imp
方法, 看下源码
.macro MethodTableLookup
SAVE_REGS MSGSEND // 注册的消息
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
// 接受者和方法已经存在 x0 和 x1 中
mov x2, x16 // mov:传送指令, 令x2 = x16
mov x3, #3 // 令x3 = #3
bl _lookUpImpOrForward // bl:跳转指令, 跳转_lookUpImpOrForward方法
// IMP in x0
mov x17, x0 // 令x17 = x0此时x0为imp
RESTORE_REGS MSGSEND //返回注册消息
.endmacro
可看出关键代码是_lookUpImpOrForward
做了些操作, 查到了imp
, 由于是一个下划线_
, 那么我们直接在底层代码查找即可。这里普及个知识点
关于下划线知识点
- 1、
C/C++
调用汇编
: 查找汇编
时,C/C++
调用的方法需要多加一个下划线
(_
→__
) - 2、
汇编
调用C/C++
方法: 查找C/C++
方法,需要将汇编调用的方法去掉一个下划线
(__
→_
)
接下来我们代码验证下
- 普通工程里面建一个类继承
NSObject
, 里面有一个对象方法sayHello
,ViewController
调用一下
- 打开汇编
Debug → Debug worlflow → Always show Disassembly
可以看到走了objc_msgSend
方法
-
control + Step into
, 进入objc_msgSend
control + Step into -
因为有很多方法都会走消息发送, 在这块我们确保进入的是
sayHello
的, 在lldb
中register read
读取寄存器一下
register read
当然我们也可以直接读取下x1
→register read x1
继续往后走
在objc_msgSend
方法中我们发现往后进入了 _objc_msgSend_uncached
方法
断点跟流程, 我们会发现还走了一个class_lookupMethodAndLoadCache3
, 之后才是lookUpImpOrForward
, iOS系统旧版本才会有这种情况, 新版本请直接略过
我们可以在objc4-750
找到这个方法(之后版本已取消这个方法)
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
可看出是一个承上启下方法, 实际上也是调用lookUpImpOrForward
汇编继续往后走, 也会发现也会走到lookUpImpOrForward
方法
不过留意下 class_lookupMethodAndLoadCache3
在objc-750
之后就取消了, ios新系统版本_objc_msgSend_uncached
中直接走lookUpImpOrForward
(下面为ios14.6测试图片)
lookUpImpOrForward
接下来我们看下慢速查找lookUpImpOrForward
方法
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
...
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
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)) {
imp = forward_imp;
break;
}
}
...
}
...
return imp;
}
可能有些人会有疑问? 为什么缓存用的是汇编?
首先缓存查找目的为了效率高, 恰巧汇编语言执行起来非常迅速/快, 同时汇编处理相对来说安全一下。
方法在下层尤其多几W, 几十W, 用汇编可以快速优化时间
3.针对参数不明确情况(C/C++方法需要参数明确), 汇编可以更加动态化的处理
而慢速查找需要不断遍历/循环 都是一些耗时操作, 所以用C++/C相对好一些, 灵活。
返回lookUpImpOrForward
代码比较多, 这里我分几篇文章, 拆分模块看。(总结在第二篇)
checkIsKnownClass
// 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);
/***********************************************************************
* 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);
}
}
这个方法是, 判断当前的类是否已经注册到缓存表里面, 即 注册类
。如果当前类未知, 直接返回错误, Attempt to use unknown class XXXX ....
realizeAndInitializeIfNeeded_locked
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
curClass = cls;
/***********************************************************************
* realizeAndInitializeIfNeeded_locked
* Realize the given class if not already realized, and initialize it if
* not already initialized.
* inst is an instance of cls or a subclass, or nil if none is known.
* cls is the class to initialize and realize.
* initializer is true to initialize the class, false to skip initialization.
**********************************************************************/
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
runtimeLock.assertLocked();
// 快速判断 当前类有没有实现
if (slowpath(!cls->isRealized())) {
// 当前类如果没实现, 调用realizeClassMaybeSwiftAndLeaveLocked, 先实现一下
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 判断类有没有初始化
if (slowpath(initialize && !cls->isInitialized())) {
// 当前类如果没实现, 调用initializeAndLeaveLocked, 先初始化一下
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
return cls;
}
这个方法比较重要, 先看一下realizeClassMaybeSwiftAndLeaveLocked
static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}
/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class.
* Locking:
* runtimeLock must be held on entry
* runtimeLock may be dropped during execution
* ...AndUnlock function leaves runtimeLock unlocked on exit
* ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
realizeClassWithoutSwift(cls, nil);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
ASSERT(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}
return cls;
}
/***********************************************************************
* 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 (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);
}
...
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);
可看到主要是先对rw
, ro
的一些数据处理, 之后处理了supercls
和metacls
。
supercls
:cls->getSuperclass()
递归取父类
, 再调用自身方法realizeClassWithoutSwift
, 即确定父类信息。metacls
:cls->ISA()
递归取元类
, 再调用自身方法realizeClassWithoutSwift
, 即确定父类信息。
之后调用cls->setSuperclass(supercls)
, cls->initClassIsa(metacls)
将当前类的父类链, 元类链确认绑定。
即知道一个类
, 变将父类/元类
所有都初始化完, 方便之后找方法。其实为了
- 对象方法: 当前
类
没有去父类
找,父类
没有再去根类
找... - 类方法: 当前
元类
没有去父元类
找,父元类
没有再去根元类
找...
二分查找
继续往后面走来到for (unsigned attempts = unreasonableClassCount();;)
, 一个死循环, 只可以通过break
, 或return
结束循环
// 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();;) {
// 判断curClass是否被缓存, 防止因为上面不断调用ro, rw被对应方法被缓存, 如果当前类可以根据 sel找到imp, 即走 if 判断,
// 不过通常是没有的
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES // IOS真机为1, 其他为0
// #if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
// #define CONFIG_USE_PREOPT_CACHES 1
// #else
// #define CONFIG_USE_PREOPT_CACHES 0
// #endif
// 如果缓存能找到对应的imp, 直接跳转 goto done_unlock 方法
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// 如果缓存没有找到对应的imp, 走二分查找
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
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
...
// 从类中获取方法列表methods
auto const methods = cls->data()->methods();
// 循环二位数组
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
...
// 从方法中查找
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)
{
// 检查mlist 是否固定顺序
int methodListIsFixedUp = mlist->isFixedUp();
// 检查mlist 是否预期大小
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;
}
...
return nil;
}
这里我们可以读一下methodListIsFixedUp
和methodListHasExpectedSize
打断点可知走的是有序方法findMethodInSortedMethodList
但是他有2个方法, 也是打断点跟一下。
可以看出, 先走的是判断是否为SmallList
。这里其实是做了一个M1机器和普通Mac机器的区分, 普通Mac机直接走下面 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(); // 初始化first为list首位
auto base = first; // 定义 base为 first
decltype(first) probe; // 定义循环查的位置 probe
uintptr_t keyValue = (uintptr_t)key; // 定义keyValue为我们查找的sel
uint32_t count; //定义count
// 循环
// count 为 list 元素个数
// 循环条件: count != 0
// 每次count >>= 1, 除2操作, 二分方法关键循环不断 / 2
for (count = list->count; count != 0; count >>= 1) {
// probe 为 base + count再除2操作
probe = base + (count >> 1);
// 定义probeValue 为查询sel
uintptr_t probeValue = (uintptr_t)getName(probe);
// 如果当前查询sel == 循环sel
if (keyValue == probeValue) {
// 做一步分类重名排查处理
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
// 返回 probe值
return &*probe;
}
// 如果keyValue > probeValue 做一步 `base = probe + 1; count--;`继续循环
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
这块我们, 结合例子来处理
例如: count = 8
进入
for
循环, 循环条件count != 0
,count >>= 1
即除2处理,
例如①: 8 = 1000 → 右移1位 → 0100 = 4
例如②: 7 = 0111 → 右移1位 → 0011 = 3probe = 0 + 4 >> 1 = 0 + 2 = 2
判断循环方法是否和查找方法一致,
keyValue == probeValue
如果满足, 即方法找到 , 做一步防止分类重名
处理,
// 这里是做的while是, 排除分类重名
// 判断 当前我已经找到的方法, 前面是否有和他一样的方法或者说同名方法(分类方法, 分类是加载到原始类前面的)
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
// 分类永远在主类前面
// 排除分类重名方法(由于方法的存储是先存储类方法,再存储分类---按照先进后出的原则,分类方法最先出,而我们要取的类方法,所以需要先排除分类方法)
// 如果是两个分类,就看谁先进行加载
probe--;
}
return &*probe;
-
如果没找到 判断
keyValue > probeValue
-
是
: 做一步base = probe + 1; count--;
继续循环 -
否
: 继续循环
-
例子1
结合一个具体例子查看, 例如: list count为8, 我们想要查找是7, 则有
第一次循环:
count = 8, base = 0, probe = 0 + 8 >> 1 = 4
判断 7 != 4, 走else
因为 7 > 4, 走if, base = 4 + 1 = 5, count = 8 - 1 = 7
第二次循环:
count = 7 >> 1 = 3, base = 8, probe = 5 + 3 >> 1 = 6
判断 7 != 6, 走else
因为 7 > 6, 走if, base = 6 + 1 = 7, count = 3 - 1 = 2
第三次循环:
count = 2 >> 1 = 0, base = 7, probe = 7 + 0 >> 1 = 7
判断 7 == 7, 走if判断, 进行分类排除, return
例子2
例如: list count为8, 我们想要查找是2, 则有
第一次循环:
count = 8, base = 0, probe = 0 + 8 >> 1 = 4
判断 2 != 4, 走else
因为 2 > 4, 走else, 继续循环
第二次循环:
count = 8 >> 1 = 4, base = 0, probe = 0 + 4 >> 1 = 2
判断 2 == 2, 走if判断, 进行分类排除, return
done
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
如果 二分查找找到imp
, 则走goto done;
, 看一下done
方法
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};
done:
// 如果当前方法没缓存
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // 64位为1, 其他为0
// 真机进行一个初始化缓存 cache_t 的操作
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
// 针对已经查找到imp直接, 调用缓存插入方法, 下次就不进慢速, 直接快速
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
真机底层调用preopt_cache_t
// 真机初始化
/* dyld_shared_cache_builder and obj-C agree on these definitions */
struct preopt_cache_t {
int32_t fallback_class_offset;
union {
struct {
uint16_t shift : 5;
uint16_t mask : 11;
};
uint16_t hash_params;
};
uint16_t occupied : 14;
uint16_t has_inlines : 1;
uint16_t bit_one : 1;
preopt_cache_entry_t entries[];
inline int capacity() const {
return mask + 1;
}
};
!!! 关键方法 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 !TARGET_OS_OSX
# define SUPPORT_MESSAGE_LOGGING 0
#else
# define SUPPORT_MESSAGE_LOGGING 1
#endif
bool objcMsgLogEnabled = false; // 初始为false
#if SUPPORT_MESSAGE_LOGGING
// objcMsgLogEnabled默认为false
// 只有调用void instrumentObjcMessageSends(BOOL flag)这方法才有机会将objcMsgLogEnabled设置为true
// 一般跳过这里直接走 cache.insert
if (slowpath(objcMsgLogEnabled && implementer)) {
// 主要做一些写日志操作
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
// 缓存插入 imp, 下次直接cache快速查找
cls->cache.insert(sel, imp, receiver);
}
我们也可以稍微看下logMessageSend
方法, 主要是对消息日志做些写入操作
bool objcMsgLogEnabled = false; // 初始为false
static int objcMsgLogFD = -1; // 初始化-1
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
// 将一些入调用方法等其他信息写在 XXXX/tmp/msgSends-XXX内
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
...
}
那么我们可以得出一个消息查找一个闭环
第一次cache
消息快速查找
没有查找到, 进入消息慢速查找
消息慢速查找
进行确认父类/元类链
之后, 进行二分查找
, 查找方法(imp)二分找到之后, 进行缓存插入操作, 下一次就直接快速查找, 不再走慢速
cache_getImp
当前类如果没有找到方法时候, 我们知道系统通常会去父类查找。但是源码是怎么操作的呢? 我们看一下
// 当前类没找到, 将curClass为curClass父类继续查找
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 如果父类也没找到令imp为forward_imp, 继续查找
imp = forward_imp;
break;
}
...
// Superclass cache.
// 调用cache_getImp方法
imp = cache_getImp(curClass, sel);
这里的话是调用了一个cache_getImp
, cache_getimp
是全局搜索一下可发现是在汇编里面, 有
可看出重新调用CacheLookup
方法, 这里要留意下
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
...
cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
...
这里要留意下父类如果没找到会调用一个MissLabelDynamic
方法, MissLabelDynamic
是传入的方法, 我们回头要看传入方法, 有:
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
// LGetImpMissDynamic 方法
LGetImpMissDynamic:
mov p0, #0 // 令p0 = 0, 返回空出去
ret // 返回
这里留意下查询父类, 不会走uncache流程。这里循环, 如果父类imp
没找到就nil
返回, 即imp = cache_getImp(curClass, sel);
返回nil
缓存没有查找到, 走for (unsigned attempts = unreasonableClassCount();;)
再次循环。
此时curClass
为父类, 那么继续往下走。Method meth = getMethodNoSuper_nolock(curClass, sel);
这个里再进行, 查找父类的方法列表 (内部调用cls->data()->methods()
)
顺序 父类快速查找→ 父类慢速 → 根类快速 → 根类慢速→ 直到 nil
(但是实际上顺序是: 父类快速查找
→ 二次循环
→ 父类慢速
→...中间其实有个再循环操作 )
总结
cache
快速查找方法没有找到走lookUpImpOrForward
慢速查找方法-
lookUpImpOrForward
中先确认父类/元类链, 再通过二分查找, 查找当前类的方法列表找到
: 方法存入缓存, 第二次直接走缓存快速查找没找到
: 去父类查找, 顺序照旧, 先快速再慢速, 有存缓存, 无继续找父类
如果父类/元类链都查找完, 还是没有, 会走动态方法决议
, 这个我放在下一篇文章来写