iOS底层-objc_msgSend慢速查找

543 阅读10分钟

前言

上篇 objc_msgSend快速查找分析了 快速查找 流程,如果快速查不到,则需要进入__objc_msgSend_uncached 慢速查找流程。

下面分析慢速查找的具体过程。

__objc_msgSend_uncached 汇编分析

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// 调用 MethodTableLookup (涉及到慢速查找逻辑)
MethodTableLookup
// 返回 imp
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
  • MethodTableLookup 调用慢速查找流程。

  • TailCallFunctionPointer 是区分机型,并跳出。

MethodTableLookup 汇编分析

.macro MethodTableLookup
    // 保护现场
    SAVE_REGS MSGSEND

    // x0 = receiver
    // x1 = sel
    // x2 = cls
    mov	x2, x16
    // x3 = 3
    mov	x3, #3
    // 调用c++层的lookUpImpOrForward
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // lookUpImpOrForward(x0, x1, x2, x3)
    bl	_lookUpImpOrForward

    // IMP in x0, 返回x17
    mov	x17, x0

    // 恢复现场
    RESTORE_REGS MSGSEND
.endmacro
  • SAVE_REGS:保护现场;RESTORE_REGS:恢复现场。常用于调用C++函数场景。

  • MethodTableLookup 记录 receiverselcls,并调用 _lookUpImpOrForward(这并不是汇编方法)。

  • 汇编 中调用 C/C++ 方法时,需要将汇编调用的方法去掉一个 下划线,即是 C/C++ 方法。

lookUpImpOrForward (慢速查找)

根据汇编查找 C++ 函数特点,将 _lookUpImpOrForward 去掉一个下划线 lookUpImpOrForward 进行全局搜索。

lookUpImpOrForward 流程的目标是找到 SEL 对应的 IMP:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // behavior = 3 (LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // 定义的消息转发imp
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();
    
    // 判断类是否初始化,其内部是查找类的元类的 bits.data().flags 的逻辑位移值。
    if (slowpath(!cls->isInitialized())) {
        // 如果没有初始化,behavior 需要增加 LOOKUP_NOCACHE 配置。
        // 发送给类的第一条消息通常是+new或+alloc或+self,新开辟的。
        // 如果不设置 LOOKUP_NOCACHE,会导致IMP缓存,永远只留下一个条目。
        behavior |= LOOKUP_NOCACHE;
    }

    // 保持runtimeLock,以防止与多线程访问冲突。
    runtimeLock.lock();

    // 判断是否是已经加载的类, 是否被dyld加载的类
    checkIsKnownClass(cls);    
    // 初始化类和元类
    // 目的是为了关联类,确定父类链,方法后续的循环
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    
    // 当前类赋值
    curClass = cls;
    
    // 获得锁之后,再次循环查找类的缓存,unreasonableClassCount: 表示类的迭代的上限
    // 死循环,一直查找,知道遇到return或者break
    for (unsigned attempts = unreasonableClassCount();;) {
        // 查找共享缓存,目的是其中某一次的缓存查找时,此时已经写入了该方法。
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// __arm64__ && IOS && !SIMULATOR && !MACCATALYST
#if CONFIG_USE_PREOPT_CACHES
            // 调用 _cache_getImp 汇编方法,从共享缓存中根据sel查询imp
            imp = cache_getImp(curClass, sel);
            // 如果能查到imp,跳转到done_unlock流程
            if (imp) goto done_unlock;
            // 获取前置类
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // 从类中查找methodlist
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 如果能找到方法,获取imp,并跳转done函数
                imp = meth->imp(false);
                goto done;
            }
            // 循环期间,查找当前类的getSuperclass(),如果父类为nil,说明当前是NSObject
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // 如果都找不到,则使用 forward_imp
                imp = forward_imp;
                break;
            }
        }

        // 如果父类中存在循环, 则停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
        // 获取父类缓存 (_cache_getImp 汇编方法)
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 如果在父类中找到了forward_imp,则停止查找,但是不缓存,首先调用此类的方法解析器。
            break;
        }
        if (fastpath(imp)) {
            // 在父类中找到imp。在这个类中缓存。
            goto done;
        }
    }

    // 如果找不到imp,执行一次方法解析
    // 执行一次的原因:
    // 第一次:behavior = 3,LOOKUP_RESOLVER = 2, 3 & 2 = 2,进入if, behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1
    // 第二次:behavior = 3,LOOKUP_RESOLVER = 2, 1 & 2 = 0,不再进入if
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        // 动态方法决议
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    // behavior = 3, LOOKUP_NOCACHE = 8, 3 & 8 = 0
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        // 从共享缓存中查找当前类
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        // 将查询到的sel和imp插入到缓存
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    // 解锁
    runtimeLock.unlock();
    // 如果 (behavior & LOOKUP_NIL)成立 且 imp == forward_imp 没有查询到直接返回 nil
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
  • 通过汇编函数 _objc_msgForward_impcache 获取 forward_imp

  • 判断类是否初始化 isInitialized(),目的:是否要对 behavior 进行增加 LOOKUP_NOCACHE 配置。原因发送给类的第一条消息通常是 +new+alloc+self ,是新开辟内存的,如果不设置 LOOKUP_NOCACHE,会导致 IMP 缓存,永远只留下一个条目。

  • 判断是否是否被dyld加载的类:checkIsKnownClassisKnownClassallocatedClasses,已经加载过的类会存储在 allocatedClasses 中。关于 allocatedClasses 后面会详细介绍。

  • 初始化类和元类 realizeAndInitializeIfNeeded_locked,目的是为了关联类,确定父类链,方便后续的循环。

  • 进入 for 查找 imp,死循环查找,只有breakreturngoto 才能停止循环,否则一直查找。

    • 查找共享缓存:调用 _cache_getImp 先快速查,快速没有再慢速查,从共享缓存中根据 sel 查询 imp。如果能找到,跳转 done_unlock

    • 查找当前类中的 methodList,如果能找到 imp ,跳转 done

    • 当前类没找到,查找父类,父类调用 _cache_getImp 先快速查,快速没有再慢速查,如果找到 imp,判断是否和 forward_imp 相等,如果相等(没找到),执行一次动态方法决议 resolveMethod_locked,如果不相等(找到),跳转 done

    • 父类没找到,查找父类的父类,直到找到NSObject,此时 imp = forward_imp(没找到),也会执行一次动态方法决议 resolveMethod_locked

  • done

    • 从共享缓存中查找当前类

    • log_and_fill_cache:将查询到的 selimp 插入到缓存。

  • done_unlock:判断 (behavior & LOOKUP_NIL) && imp == forward_imp) 是否成立,成立则直接返回 nil,否则返回 imp

_objc_msgForward_impcache 汇编分析

STATIC_ENTRY __objc_msgForward_impcache

// 将__objc_forward_handler的基址 读入x17寄存器
adrp x17, __objc_forward_handler@PAGE
// 读取 Forward IMP (x17 = x17 + __objc_forward_handler的基址偏移量)
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
// 返回 Forward IMP
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward_impcache

全局搜索 __objc_forward_handler 并没有,那么搜索C函数 _objc_forward_handler,如下:

根据慢速查找流程,当自身类和父类都没找到时,则 imp = forward_imp,也就是unrecognized selector sent to instance 报错原因。

isInitialized() 分析

bool isInitialized() {
    // 元类的 class_rw_t 的 flags 状态
    // RW_INITIALIZED = (1<<29)
    return getMeta()->data()->flags & RW_INITIALIZED;
}

判断类是否初始化,查看其元类中 class_rw_t 中的 flags 状态,以及 RW_INITIALIZED 的值。

realizeAndInitializeIfNeeded_locked

类的加载和初始化,后续详细研究。此处目的是准备好类的信息,主要是方法列表,便于后面查找。

resolveMethod_locked

动态方法决议,后续详细探究。

log_and_fill_cache() 分析

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);
}
  • 日志打印

  • cache.insert 缓存插入

getMethodNoSuper_nolock

getMethodNoSuper_nolock(Class cls, SEL sel) {
    // 获取类的方法列表 methodlist
    auto const methods = cls->data()->methods();
    // 循环查找,元素对象是 method_list_t,有可能是二维数组。(动态加载方法和类导致的)
    for (auto mlists = methods.beginLists(), end = methods.endLists(); mlists != end; ++mlists) {
        // 查找 method_list_t 内联方法 ( method_list_t 有可能是二维数组 )
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
  • 获取类的方法列表methods():存于类的 class_rw_t 结构体中。

  • 循环查找,有可能是二维数组。(动态加载方法或者动态加载类导致)

  • search_method_list_inline 方法是通过两种方式获取 imp,一种的顺序的,一种是非顺序的。

search_method_list_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 {
        // 否则线性查找未排序的方法列表
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }
    return nil;
}
  • 判断方法列表是否已排序

  • 线性查找: findMethodInUnsortedMethodListfindMethodInUnsortedMethodListfor 循环获取imp。

  • 二分查找: findMethodInSortedMethodListfindMethodInSortedMethodList → 二分查找 。

二分查找方法 (重点)

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName) {
    // 二分查找方法
    auto first = list->begin(); // 第一个方法的位置 0
    auto base = first; // 起始位置 0
    decltype(first) probe; // 每一个遍历到的指针
    uintptr_t keyValue = (uintptr_t)key; // 传入的指针
    uint32_t count; // 假设是8
    for (count = list->count; count != 0; count >>= 1) {
        // prode = 基数 + 总数 >> 1
        probe = base + (count >> 1);
        // 获取prode的name值
        uintptr_t probeValue = (uintptr_t)getName(probe);
        // 如果是和传入的值相等
        if (keyValue == probeValue) {
            // 查找分类同名sel。如果匹配了就找分类中的。因为分类是在前面的,所以一直找到最开始的位置。
            // (方法的存储是先存储类方法,再存储分类,按照先进后出的原则。)
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                // 如果是两个分类,就看谁先进行加载
                probe--;
            }
            // 返回指针地址
            return &*probe;
        }
        // 如果目标值 > 遍历的值
        if (keyValue > probeValue) {
            // 基数 = 当前prode + 1
            base = probe + 1;
            // 总数--
            count--;
        }
    }
    return nil;
}
  • first:方法列表的起始位置,用于比较。

  • base:一个基值,为了少做比较,当要继续往后查找的时候 base 为当前查找元素的下一个元素。

  • probe:每次循环的临时变量,用户获取 selkey 做比较。

  • count >>= 1:相当于 count / 2

代码模拟

int findSortedMethodList(int key, int listCount) {
    int first = 0; // 起始位置
    int base = 0; // 基准位置
    int probe = 0; // 每一个遍历到的数
    int times = 0;
    printf("目标key = %d\n", key);
    for (int count = listCount; count != 0; count >>= 1) {
        times++;
        // 遍历到的值
        probe = base + (count >> 1);
        printf("第%d次: count = %d, base = %d, probe = %d\n", times, count, base, probe);
        // 如果是和传入的值相等
        if (key == probe) {
            // 找到
            printf("找到了! probe = %d\n", probe);
            return probe;
        }
        // 如果目标值 > 遍历的值
        if (key > probe) {
            // 基数 = 当前prode + 1
            base = probe + 1;
            // 总数--
            count--;
        }
    }
    printf("没找到\n");
    return -1;
}

慢速查找流程图

案例

  • 定义一个类 ZLObject 和 其子类 ZLSubObject
@interface ZLObject : NSObject

+ (void)classMethod;

- (void)instanceMethod1;

- (void)instanceMethod2;

@end

@implementation ZLObject

+ (void)classMethod {
    NSLog(@"%s",__func__);
}

- (void)instanceMethod1 {
    NSLog(@"%s",__func__);
}

@end
@interface ZLSubObject : ZLObject

- (void)subInstanceMethod;

@end

@implementation ZLSubObject

- (void)subInstanceMethod {
    NSLog(@"%s",__func__);
}

@end
  • 添加调用方法
ZLSubObject *subObj = [[ZLSubObject alloc] init];
[subObj subInstanceMethod];
[subObj instanceMethod1];
[subObj instanceMethod2];
  • 打印结果
-[ZLSubObject subInstanceMethod]
-[ZLObject instanceMethod1]
-[ZLSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006440e0

subInstanceMethodinstanceMethod1 实现了方法,可以正常打印

instanceMethod2 没有实现方法,因此崩溃。

  • 分析堆栈如下:

崩溃在 doesNotRecognizeSelector 方法,并且是个对象方法。

  • 在源码中搜索 doesNotRecognizeSelector

  • 添加一个 NSObject 的分类
@interface NSObject (AddMethod)

- (void)categoryInstanceMethod;

@end

@implementation NSObject (AddMethod)

- (void)categoryInstanceMethod {
    NSLog(@"%s",__func__);
}

@end
  • 添加调用方法
[ZLSubObject performSelector:@selector(categoryInstanceMethod)]
  • 打印结果
-[NSObject(AddMethod) categoryInstanceMethod]

执行成功,并且还是对象方法。

原因:涉及到了类的 继承链 ,当前类执行类方法,其实就是元类执行对象方法,根据上面分析,如果当前类没找到 imp,会查找父类的方法,NSObject 元类的父类还是 NSObject 类,所以能找到对应的代理方法。

疑问点

1. objc_msgSend 为什么要用汇编?而不是用C/C++?

  • 汇编更接近机器语言,执行速度更快。
  • 安全性较高。
  • 更加动态化。比如C语言调用方法时,参数必须传值,但是汇编有些参数无需传值,如果参数列表中最后一个。

2. 二分查找之前是需要排序的,排序是什么时候完成的?

method_t 结构体中,搜索 sort 相关的关键字。发现了如下方法:

struct SortBySELAddress :
public std::binary_function<const struct method_t::big&,
                            const struct method_t::big&, bool> {
    bool operator() (const struct method_t::big& lhs,
                     const struct method_t::big& rhs)
    { return lhs.name < rhs.name; }
};

打断点后发现了如下调用栈:

结论:是在_read_images类加载映射的时候注册调用该方法进行的排序,是按照 sel地址 进行排序的。

补充

realizeAndInitializeIfNeeded_locked

在慢速查找流程执行之前,首先使用 realizeAndInitializeIfNeeded_locked 初始化类,便于获取 methods,其流程为:realizeAndInitializeIfNeeded_lockedinitializeAndLeaveLockedinitializeNonMetaClasscallInitialize

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}

结论:当执行 callInitialize 时,会将当前类的 initialize 方法,通过 objc_msgSend 进行系统调用。

拓展:系统调用的方法有 load方法构造方法initialize,会由系统自动调用。

父类方法查找流程

在上面慢速查找流程中,其中如果当前类没找到 imp,会查找父类的 cache_imp,如果没找到,会继续执行 for 循环,那么第二次进来的时候,会执行 父类getMethodNoSuper_nolock。如果 父类 也没有,那么继续查找 父类的父类, 以此类推,直到找到根类 NSObject 以及 NSObject 的父类 nil