lookUpImpOrForward 消息慢速查找

上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。

一、汇编中找不到缓存

在汇编代码中只有_lookUpImpOrForward的调用而没有实现,代码中直接搜这个也是搜不到的。因为实现在c/c++代码中,需要搜索lookUpImpOrForward。声明如下:

extern IMP lookUpImpOrForward(id obj, SEL, Class cls, int behavior);

那么参数肯定也就是汇编中传过来的,汇编中调用如下:

.macro MethodTableLookup
    
    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    //x2 = cls
    mov x2, x16
    //x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
    //_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    mov x3, #3
    bl  _lookUpImpOrForward

    // IMP in x0
    mov x17, x0

    RESTORE_REGS MSGSEND

.endmacro
  • 3个参数没有什么好说的,behaviorLOOKUP_INITIALIZE | LOOKUP_RESOLVER。那就证明lookUpImpOrForward是有查找模式的。
  • 调用完_lookUpImpOrForward后有mov x17, x0说明是有返回值的,与c/c++lookUpImpOrForward的声明对应上了。

那么就有一个问题了,为什么cache查找要使用汇编?
1.汇编更接近机器语言,执行速度快。为了快速找到方法,优化方法查找时间。
2.消息发送参数是未知参数(比如可变参数),c参数必须明确,汇编相对能够更加动态化。
3.更安全。

二、 慢速查找流程

慢速查找就是不断遍历methodlist的过程,遍历是一个耗时的过程,所以是使用c/c++来实现的。

2.1 lookUpImpOrForward

首先明确慢速查找流程的目标是找到sel对应的imp。所以核心就是lookUpImpOrForward中返回imp的逻辑,精简后源码如下:

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //forward_imp赋值
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    //要返回的imp
    IMP imp = nil;
    //当前查找的cls
    Class curClass;

    //初始化的一些处理,如果类没有初始化behavior会增加 LOOKUP_NOCACHE,判断是否初始化取的是data()->flags的第29位。
    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }

    //类是否已经注册,注册后会加入allocatedClasses表中
    checkIsKnownClass(cls);
    //初始化需要的类。由于要去类中查找方法,如果rw,ro没有准备好那就没有办法查了。也就是为后面的查找代码做好准备。LOOKUP_INITIALIZE用在了这里
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    //赋值要查找的类
    curClass = cls;
    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {//……}

    //参数LOOKUP_RESOLVER用在了这里,动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    //没有初始化LOOKUP_NOCACHE就有值了,也就是查完后不要插入缓存。在这个流程中是插入
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        //共享缓存
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        //填充缓存,这里填充的是`cls`。也就是父类如果有缓存也会被加进子类。
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    //forward_imp 并且有 LOOKUP_NIL 的时候直接返回nil。也就是不进行forward_imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
  • 先给forward_imp赋值_objc_msgForward_impcache,这个函数的实现是在汇编中。
  • impcurClass定义。
  • cls->isInitialized()类没有初始化则behavior增加LOOKUP_NOCACHE,类有没有初始化时由data()->flags的第29位决定的。
    bool isInitialized() {
        //#define uint32_t RW_INITIALIZED (1<<29)
        return getMeta()->data()->flags & RW_INITIALIZED;
    }
  • checkIsKnownClass判断类是否已经注册,注册后会加入allocatedClasses表中。
  • realizeAndInitializeIfNeeded_locked初始化需要的类,由于要去类中查找方法,如果rw ro没有准备好那就没有办法查了(methods就存在其中)。也就是为后面的查找代码做好准备。汇编中调用的时候传递的behaviorLOOKUP_INITIALIZE用在了这里。它的流程会在后面介绍。
  • 进入for死循环查找imp,核心肯定就是找imp赋值的地方了。那么就只有breakreturngoto才能停止循环,否则一直查找。
  • 如果上面imp没有找到,LOOKUP_RESOLVER是有值的,会进入动态方法决议。
  • 如果找到imp会跳转到done,判断是否需要插入缓存会调用log_and_fill_cache最终调用到cache.insert。父类如果有缓存找到也会加入到子类,这里是因为写入的时候参数是cls
  • 根据LOOKUP_NIL判断是否需要forward,不需要直接返回nil,需要返回imp

2.1.1 behavior 说明

在从汇编调入lookUpImpOrForward的时候传入的behavior参数是LOOKUP_INITIALIZELOOKUP_RESOLVER
behavior类型如下:

/* method lookup */
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

根据上面的分析可以得到大致结论:

  • LOOKUP_INITIALIZE: 控制是否去进行类的初始化。有值初始化,没有不初始化。
  • LOOKUP_RESOLVER:是否进行动态方法决议。有值决议,没有值不决议。
  • LOOKUP_NIL:是否进行forward。有值不进行,没有值进行。
  • LOOKUP_NOCACHE:是否插入缓存。有值不插入缓存,没有值插入。

2.2 realizeAndInitializeIfNeeded_locked

在这里主要进行类的实例化和初始化,有两个分支:RealizeInitialize

2.2.1 Realize

(这个分支一般在_read_images的时候就处理好了)
在进行类的实例化的时候调用流程是这样的realizeAndInitializeIfNeeded_locked->realizeClassMaybeSwiftAndLeaveLocked->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift,最终会调用realizeClassWithoutSwiftswift会调用realizeSwiftClass。这个不是这篇文章的重点,分析下主要代码如下:

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    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);
    }
    //赋类和元类的操作
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

    //关联类
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);
    return cls;
}
  • 对类的ro以及rw进行处理。
  • 循环调用了父类和元类的realizeClassWithoutSwift
  • 关联了父类和元类。

当对象调用方法的时候判断类是否初始化,如果初始化了再判断类的父类以及元类,相当于是递归操作了,一直到NSObject->nil为止。也就是说只要有一个类进行初始化它的上层(也就是父类和元类)都会进行初始化,是一个连锁反应。

⚠️为什么这么操作?
就是为了查找方法。类没有实例方法的话会找父类,类没有类方法会找元类,所以需要这么操作。

2.2.2 Initialized

realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass。在initializeNonMetaClass中调用了callInitialize(cls)

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

系统直接objc_msgSend发送了initialize消息。所以initialize是在类第一个方法被调用的时候进行调用的。也就是发送第一个消息的时候:
消息慢速查找开始前进行类初始化的时候发送的initialize消息

三、循环查找

对于慢速查找流程,我们想到的就是先查自己然后再查父类一直找到NSObject->nil
慢速查找流程应该是这样:
1.查自己methodlist->(sel,imp)。
2.查父类->NSObject->nil ->跳出来

查看源码:

//死循环,除非return/break
for (unsigned attempts = unreasonableClassCount();;) {
    //先去共享缓存查找,防止这个时候共享缓存中已经写入了该方法。
    if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
        //这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找共享缓存。
        imp = cache_getImp(curClass, sel);
        //找到后直接跳转done_unlock
        if (imp) goto done_unlock;
        curClass = curClass->cache.preoptFallbackClass();
#endif
    } else {
        // curClass method list.进行循环查找
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        //找到method
        if (meth) {
            //返回imp
            imp = meth->imp(false);
            //跳转done
            goto done;
        }
        //这里curClass 会赋值,直到找到 NSObject->nil就会返回forward_imp
        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
            imp = forward_imp;
            break;
        }
    }

    // Superclass cache.
    imp = cache_getImp(curClass, sel);
    if (slowpath(imp == forward_imp)) {

        break;
    }
    if (fastpath(imp)) {
        goto done;
    }
}
  • 可以看到这是一个死循环。
  • 如果有共享缓存,先查找共享缓存,因为前面做了很多准备工作,防止这个时候共享缓存中已经写入了该方法(在汇编中已经查过一次了)。
  • 否则就进行二分查找流程,核心逻辑是在getMethodNoSuper_nolock中调用的,查找完成返回。
  • 如果找到method则获取imp跳转done,如果没有找到将父类赋值给curClass,父类不存在则imp = forward_imp;
    • 找到则进入找到imp done的逻辑。
      • log_and_fill_cache插入缓存,也就是调用cls->cache.insert与分析cache的时候逻辑对上了。
      • 返回imp
    • 没有找到则curClass赋值superclass,没有superclass也就是找到了NSObject->nil的情况下imp = forward_imp
    • 没有找到并且有父类的情况下通过cache_getImp去父类的cache中查找。这里与共享缓存的cache_getImp是一个逻辑,最终都是调用汇编_cache_getImp->CacheLookup

    父类也有快速和慢速查找。

  • 如果父类中也没有找到,则进入递归。直到imp找到或者变为forward_imp才结束循环。

_cache_getImp 说明
源码:

    STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0, 0
    CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

LGetImpMissDynamic:
    mov p0, #0
    ret

LGetImpMissConstant:
    mov p0, p2
    ret

    END_ENTRY _cache_getImp

最终也是调用CacheLookup进行缓存查找。但是第三个参数是LGetImpMissDynamic实现是mov p0, #0 ret也就是找不到就返回了。不会去走__objc_msgSend_uncached逻辑。

⚠️ 找到父类缓存会插入自己的缓存

3.1 二分查找流程

3.1.1 getMethodNoSuper_nolock

首先进入的是getMethodNoSuper_nolock,实现如下:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    //获取methods
    auto const methods = cls->data()->methods();
    //循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
    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;
}
  • 这里只是普通的循环,因为methods获取的数据类型是method_array_t,它存储的是method_list_t。这里的数据结构有可能是二维数据,因为动态加载方法和类导致。
  • 核心逻辑是调用search_method_list_inline实现的

3.1.2 search_method_list_inline

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    //methodlist是否已经修复
    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;
    }
    return nil;
}
  • 首先判断有序无序。
  • 有序进入二分查找findMethodInSortedMethodList
  • 无序进入循环查找findMethodInUnsortedMethodList

⚠️ 那么就有个问题,排序是什么时候完成的?
既然是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; }
    };

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

image.png

是在_read_images类加载映射的时候注册调用的。又见到了_read_images,这个方法将在后面继续研究。

结论:类在加载实例化的时候进行的排序,是按照sel address进行排序的。

3.1.3 findMethodInSortedMethodList 二分查找

findMethodInSortedMethodList会根据架构最终进入各自的findMethodInSortedMethodList方法,这里以x86为例进行分析:

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;
    //method list count
    uint32_t count;
     //count >>= 1 相当于除以2。加入count为8
    for (count = list->count; count != 0; count >>= 1) {//7 >> 1 = 3,前一个已经比较了4,这里就完美的避开了4。
        //base是为了配合少查找
        //probe中间元素,第一次 probe = 0 + 8 >> 1 = 4
        probe = base + (count >> 1);
        //sel
        uintptr_t probeValue = (uintptr_t)getName(probe);
        //与要查找的sel是否匹配
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            //查找分类同名sel。如果匹配了就找分类中的。因为分类是在前面的,所以一直找到最开始的位置。
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            //匹配则返回。
            return &*probe;
        }
        //没有匹配
        if (keyValue > probeValue) {//大于的情况下,在后半部分
            //没有匹配的情况下,如果sel在后半部分,这里base指向了上次查找的后面一个位置。
            base = probe + 1;//5
            //count对应减1
            count--;//7 -- 操作为了少做比较,因为已经比较过了
        }
        //在前半部分不进行额外操作。
    }
    
    return nil;
}
  • 首先是一个循环比较,条件是count >>= 1,这里是对count进行减半,相当于二分。
  • base是为了少做比较,相当于是一个基线,当要继续往后查找的时候base为当前查找元素的下一个元素。
  • 当要继续往后查找的时候count进行了--操作,这一步是为了count >>= 1不包含已经比较过的范围。
  • 找到值的时候会循环继续往前查找,因为存在分类与类中方法同名的情况(分类方法放在类中同名方法前面),一直找到不同名为止。

⚠️根据源码可以得到以下结论:
1.分类方法调用先找类中方法,再逐次找到分类方法,直到找到第一个。
2.因为判断条件是当前命中元素与前一个元素比较,sel相同的情况下才继续查找,那就说明分类的方法是插入类中方法列表中的,都在对应类中方法的前面。

  • 查找完毕后会返回lookUpImpOrForward

这里以有8个方法为类来分析查找流程,过程如下:

比较开始值:count = 8 base = 0 probe = 4
- 第一次:比较 probe = 4
    - keyValue > probeValue  count = 3(先--,再>>1) base = 5  probe = 6
        第二次: 比较 probe = 6
        - keyValue > probeValue count = 1(先--,再>>1) base = 7  probe = 7
            第三次:比较 probe = 7
                - keyValue > probeValue count = 0(先--,再>>1) base = 8  probe = 8(count == 0)
                - keyValue < probeValue count = 0(>>1) base = 7  probe = 7 (count == 0)
        - keyValue < probeValue count = 1(>>1) base = 5  probe = 5
            第三次:比较 probe = 5
                - keyValue > probeValue count = 0(先--,再>>1) base = 6  probe = 6 (count == 0)
                - keyValue < probeValue count = 0(>>1) base = 5  probe = 5(count == 0)
    - keyValue < probeValue count = 4(>>1) base = 0  probe = 2
        第二次:比较 probe = 2
        - keyValue > probeValue count = 1(先--,再>>1) base = 3  probe = 3
            第三次:比较 probe = 3
                - keyValue > probeValue count = 0(先--,再>>1) base = 4  --(count == 0)
                - keyValue < probeValue count = 0(>>1) base = 3  -- (count == 0)
        - keyValue < probeValue count = 2(>>1) base = 1  probe = 1
            第三次:比较 probe = 1
                - keyValue > probeValue count = 0(先--,再>>1) base = 0  --(count == 0)
                - keyValue < probeValue count = 1(>>1) base = 0 probe = 0
                    第四次:比较 probe = 0
                        - keyValue > probeValue count = 0(先--,再>>1) base = 1  --(count == 0)
                        - keyValue < probeValue count = 0(>>1) base = 0 -- (count == 0)

代码模拟:

int testFindSortedMethods(int methodCount,int findKey) {
    int base = 0;
    int probe = 0;
    int round = 0;
    printf("查找key:%d\n",findKey);
    for (int count = methodCount; count != 0; count >>= 1) {
        round++;
        probe = base + (count >> 1);
        printf("\t第%d轮 scan count :%d, base:%d,probe:%d\n",round,count,base,probe);
        if (findKey == probe) {
            printf("\tfound prode:%d\n",probe);
            return probe;
        }
        if (findKey > probe) {
            base = probe + 1;
            count--;
        }
    }
    printf("\tnot found -1\n");
    return -1;
}

调用:

testFindSortedMethods(8, 0);
testFindSortedMethods(8, 1);
testFindSortedMethods(8, 2);
testFindSortedMethods(8, 3);
testFindSortedMethods(8, 4);
testFindSortedMethods(8, 5);
testFindSortedMethods(8, 6);
testFindSortedMethods(8, 7);
testFindSortedMethods(8, 8);
testFindSortedMethods(8, 9);

输出:

查找key:0
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :2, base:0,probe:1
    第4轮 scan count :1, base:0,probe:0
    found prode:0
查找key:1
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :2, base:0,probe:1
    found prode:1
查找key:2
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    found prode:2
查找key:3
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :1, base:3,probe:3
    found prode:3
查找key:4
    第1轮 scan count :8, base:0,probe:4
    found prode:4
查找key:5
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:5,probe:5
    found prode:5
查找key:6
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    found prode:6
查找key:7
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    found prode:7
查找key:8
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    not found -1
查找key:9
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    not found -1

可以看到输出与验证的结论一致。

流程图:


二分查找案例示意图

四、案例分析慢速查找流程

定义一个HPObject以及它的子类HPSubObject
HPObject定义和实现如下:

@interface HPObject : NSObject

- (void)instanceMethod1;

- (void)instanceMethod2;

+ (void)classMethod;

@end

@implementation HPObject

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

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

@end

HPSubObject定义和实现如下:

@interface HPSubObject : HPObject

- (void)subInstanceMethod;

@end

@implementation HPSubObject

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

@end

根据前面分析的方法查找逻辑测试代码:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
        HPSubObject *subObject = [HPSubObject new];
        //对象方法 根据慢速查找分析是能找到的
        [subObject subInstanceMethod];
        [subObject instanceMethod1];
        [subObject instanceMethod2];
#pragma clang diagnostic pop

输出:

-[HPSubObject subInstanceMethod]
-[HPObject instanceMethod1]
-[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0'
  • subInstanceMethodinstanceMethod1符合预期。
  • instanceMethod2找不到报错unrecognized selector sent to instance,为什么报这个错误呢?
    查看调用堆栈如下:
    image.png

    那么去源码中搜索错误信息找到以下内容:
// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

// Default forward handler halts the process.
__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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

那么调用的是哪个呢?断点后并没有进入。根据上面的分析imp找不到的时候会有两个选项resolveMethod_locked或者_objc_msgForward_impcache
_objc_msgForward_impcache的汇编实现如下:

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b   __objc_msgForward

END_ENTRY __objc_msgForward_impcache

内部直接调用了__objc_msgForward

ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

可以看到__objc_msgForward的实现是调用__objc_forward_handler,也就是:

// Default forward handler halts the process.
__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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

这也就是报错信息的原因,里面进行了格式化的错误信息打印。

接着增加一个NSObject的分类:

@interface NSObject (Additions)

- (void)categoryInstanceMethod;

@end

@implementation NSObject (Additions)

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

@end

调用:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
        [HPSubObject classMethod];
        [HPSubObject performSelector:@selector(categoryInstanceMethod)];
#pragma clang diagnostic pop

输出:

+[HPObject classMethod]
-[NSObject(Additions) categoryInstanceMethod]
  • classMethod类方法能找到符合预期。
  • 为什么HPSubObject能调用categoryInstanceMethod实例方法?
    这就涉及到了类的继承链,NSObject元类的父类是NSObject类,所以能找到。

再次说明OC的底层没有实例和类方法的区分,类方法和实例方法是人为加上去的。我们只是为了配合OC的演出视而不见。

五、 总结

慢速查找流程:

  • checkIsKnownClass检查注册类。
  • realizeAndInitializeIfNeeded_locked初始化类,为方法查找做好准备。
  • 递归查找imp,会涉及到动态缓存库的二次确认以及父类的快速慢速查找。
    • 查找过程会进行二分查找/递归查找。
    • 是否二分要看方法列表是否已经排序,排序操作是在类加载实例化的时候完成的。
    • 二分查找算法很经典,充分利用>>1以及--不多浪费一次机会。
  • 找到imp直接跳转返回。根据LOOKUP_NOCACHE判断是否插入缓存。
  • 没有找到则判断是否进行动态方法决议。
  • 不进行动态方法决议则判断是否要forward

整个逻辑流程图:


lookUpImpOrForward慢速消息查找
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容