iOS 底层 - 一文读懂OC方法查找与消息转发

3,879 阅读13分钟

前言

方法查找 , 动态方法解析以及消息转发已经是面试常客了 , 也是我们了解 Aspects 或者饿了么的 AOP - Stinger 等等优秀的三方库必不可少的基础知识 .

上篇文章 手把手带你探索OC方法的本质 我们讲到 objc_msgSend 函数汇编查找方法缓存的流程 , 直到缓存没有命中 , 由 JumpMiss 调用了 bl __class_lookupMethodAndLoadCache3 , 由此找到了 lookUpImpOrForward 回到了 c 函数中 , 开启方法查找与消息转发的流程 .

  • 建议 :

开启本篇文章探索前 , 请对 OC类对象/实例对象/元类 各自存储着什么内容 , 以及 isa 的走位有详细了解 .

1、方法查找流程

继续 上篇文章 , 我们 objc_msgSend 汇编查找缓存 , 缓存 miss 时来到 _class_lookupMethodAndLoadCache3 这个函数 ,

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    /*
     cls:如果是实例方法那就是类,如果是类方法那就是元类
     sel:方法名
     obj:方法调用者
     */
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

首先需要明白的是 :

  • 当调用实例对象方法时 , 查找的将是类对象 .
  • 当调用类方法是 , 查找的将是元类对象 .
  • 注意 : 本文下面书内容中所指 "本类" 基于此前提 . 就是说当调用实例方法 , 本类就是指类对象 , 当调用类方法 , 本类就是指元类 .

lookUpImpOrForward

注意 : runtime 有两个版本 , 找源码的时候不要找到 objc-class-old.mm 中去了 .

1.1 - 概览

源码如下 :

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    //如果需要从缓存里面查找,那需要先从缓存里面找
    // 第一次进入为 false , 因为汇编快速查找流程没找到进入.
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp; // 找到就直接返回 imp
    }
    // 加把锁 
    runtimeLock.lock();
    checkIsKnownClass(cls);
    
    /** - 为查找方法做准备条件,判断类有没有加载好
        - 如果没有加载好,那就先加载一下类信息,准备好父类、元类
        - 只会加载一次. 
        - 具体可以参考 realizeClass 具体实现.
       */
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    
    // 当 sel == initialize, _class_initialize 将会调用 +initialize
    // 确保对象已经初始化
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
    
 retry:    
    /** 这部分代码过长先省略 , 后面详细分析...*/
 done:
    runtimeLock.unlock();
    return imp;
}

具体操作都加了注释.

  • 先来看下调用普通实例方法时 , 该方法的参数情况 .

第一次进入该函数是由 objc_msgSend 汇编快速查找完 cache , 然后 cache miss 才会来到 , 因此传入 cachefalse .

  • 源码分析 :
    • 1️⃣ : 如果指定需要查找缓存 , 就先查一次缓存 , 找到直接 return, 找不到继续下面步骤 ( 第一次进来为不查找缓存 , 因为没有缓存 ) .
    • 2️⃣ : 加锁 , 确保类已经初始化 ,准备好父类 , 元类 .

1.2 - retry

1.2.1 - 查找本类

// 查找本类缓存
imp = cache_getImp(cls, sel);
if (imp) goto done;

// 查找本类方法列表 class_data_bits_t -> bits.
{
    Method meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
}
  • 源码分析 :
    • 1️⃣ : 查找本类缓存 , 找到直接跳转到 done, 找不到继续下面步骤.
    • 2️⃣ : 查找本类方法列表 class_data_bits_t 中 , 找到后进行缓存并打印 , 直接跳转到 done , 找不到继续下面步骤 .

1.2.2 - 循环查找父类

{
    unsigned attempts = unreasonableClassCount();
    for (Class curClass = cls->superclass;
         curClass != nil;
         curClass = curClass->superclass)
    {
        if (--attempts == 0) {
            _objc_fatal("Memory corruption in class list.");
        }
        
        // 查找父类缓存
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // 找到 -> 把方法缓存,跳转到 done
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                break;
            }
        }
        
        // 查找父类方法列表
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
}
  • 源码分析 :
    • 1️⃣ : 循环遍历父类 , 查找方法缓存 , 找到直接跳转到 done, 找不到继续下面步骤.
    • 2️⃣ : 查找方法列表 , 找到把方法添加到缓存并打印 , 直接跳转到 done , 找不到继续遍历 .
    • 3️⃣ : 直到遍历结束 superclass 为 nil , 继续下面流程 .

1.3 动态方法解析

retry 部分经过 本类 , 父类方法缓存和方法列表查找都没找到时来到此处. 代码如下:

if (resolver  &&  !triedResolver) {
    runtimeLock.unlock();
    _class_resolveMethod(cls, sel, inst);
    runtimeLock.lock();
    // Don't cache the result; we don't hold the lock so it may have 
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
}

我们可以看到,在经过 _class_resolveMethod 后,会进行一遍 retry 操作,重新进行一遍方法的查找流程,并且只有一次动态方法解析的机会 .

官方给出注释如下 :

Don't cache the result ; we don't hold the lock so it may have changed already. Re-do the search from scratch instead .

我们先来看下 _class_resolveMethod 的实现 , 再来看这个问题 .

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

源码流程分析 :

  • 如果本类为元类 : 说明方法为类方法 . 调用 _class_resolveInstanceMethod

  • 如果本类不是元类 : 说明方法为实例方法 . 调用 _class_resolveClassMethod

    • 调用 lookUpImpOrNil 如果没找到 , 调用 _class_resolveInstanceMethod

1.3.1 _class_resolveInstanceMethod

源码如下 :

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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 {
            _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));
        }
    }
}

源码流程分析 :

  • 判断是否实现 SEL_resolveInstanceMethod 方法,即 +(BOOL)resolveInstanceMethod:(SEL)sel , 提示 , NSObject 已经实现这个方法 , 默认返回为 NO
  • 向本类发送 SEL_resolveInstanceMethod 消息 , 即调用这个方法 .
  • 调用解析器方法 ( SEL_resolveInstanceMethod ) 完成后 , 重新检查有没有这个 selimp .
  • 该方法执行完 , 回归到 lookUpImpOrForward 中 , 进行 retry , triedResolver 设置为 YES .

讲到这里 , 是不是就对我们刚刚所说的 : 在经过 _class_resolveMethod 后,会进行一遍 retry 操作,重新进行一遍方法的查找流程,并且只有一次动态方法解析的机会 豁然开朗了 .

1.3.2 _class_resolveClassMethod

这个方法和 1.3.1 - _class_resolveInstanceMethod 基本一样 , 只不过发消息的对象变成元类 , 如下图. 这里就不重复赘述了 .

1.4 消息转发入口

retry 最后一步中源码如下 :

// No implementation found, and method resolver didn't help. 
// Use forwarding.

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

lookUpImpOrForward 方法中,经过对本类 , 父类的缓存,方法列表查找和动态方法解析后,如果以上步骤都没有进行处理,那么就会进入,消息处理的最后一步,即消息转发流程,这也是苹果预留的补救方法崩溃最后一步。

1.5 done

找到 imp , 返回.

done:
    runtimeLock.unlock();
    return imp;

问题

为什么当调用类方法时 , 对元类进行了动态方法解析也没有找到 imp 时 , 还要再对类进行实例方法的解析呢 ? 如下图 :

答 :

  • 其实这是由于 isa 走位的原因 , 根源类的父类是 NSObject ,

  • 类方法和实例方法的区别是存储地不同 , 类方法是在元类中 , 实例方法是在类对象中 , 都是存储在方法列表里 , 并无 + / - 之分 .

  • 因此当调用类方法时 , 继承链走到根源类并没有查找到时 , 是需要继续走其父类 NSObject 查一次动态方法解析器的 , 而 NSObject 作为类对象 , 是需要通过实例方法去查找的 .

总结 :

也就是说 , 当在本类和父类中方法缓存以及方法列表都没有找到 imp 时 , 苹果给了我们一次动态方法解析的机会 , 也就是在 resolveInstanceMethod 中自己给这个方法添加 imp , return true 即可 .

2、消息转发

在上面流程中 , 都没有找到函数实现地址 , 那么就进入了消息转发流程 , 调用入口如下 :

_objc_msgForward_impcacheobjc_msgSend 一样 , 是由汇编实现的 , 因此全局搜索 , 直接来到 入口处.

2.1 _objc_msgForward_impcache

代码如下 :

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache

还记得上篇文章提到的 汇编函数入口和结束符吧 ENTRY , END_ENTRY .

  • 这段函数很明显 只调用了 __objc_msgForward .

  • 搜索 __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_forward_handlerimp 放到 x17 寄存器中 .
  • x17 寄存器中将低 32 位的数据放到 p17 里 .
  • 调用 x17 存储的函数 imp .

也就是说我们需要找 _objc_forward_handler .

2.2 _objc_forward_handler

全局搜索 , 在 objc-runtime.mm 中找到如下 :

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

再搜索 objc_defaultForwardHandler , 找不到啥时候赋值的了 , 跟不进去了 ..

怎么办呢 ?

别急 , 既然前辈们都已经探索出来了 , 必然是有地方可以寻找点蛛丝马迹的 . 回顾一下本篇文章方法查找流程里 , 我们忽视掉一些细节 .

在整个流程里 , 都是有一个打印的. 那么打印到哪儿了呢 ? 点进去看下 .

2.3 打印查找

2.3.1 log_and_fill_cache

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

其中 SUPPORT_MESSAGE_LOGGING 为 :

#   define SUPPORT_MESSAGE_LOGGING 1

也就是说 objcMsgLogEnabled 只要为 true , 就可以打印了 , 那么怎么把它设置为 true 呢 ?

2.3.2 instrumentObjcMessageSends

cmd + 点击 objcMsgLogEnabled . 来到 objc-class.mm . 搜索 objcMsgLogEnabled , 我们看到有如下函数 .

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;
}

这里我们找到 当调用这个函数 , 参数会被赋值给 objcMsgLogEnabled .

因此来到我们的代码中 .

修改代码如下 :

#import "LBStudent.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LBStudent *student = [[LBStudent alloc] init];
        
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);
        
    }
    return 0;
}

运行工程 . 来到 /private/tmp 目录下 , 打开最新的一份 msgSends 文件 :

打开如下 :

2.4 快速转发流程

这其中 resolveInstanceMethod 动态方法解析我们已经分析过了 , 来到 forwardingTargetForSelector , 搜索一下发现其只有在 NSObject 中默认实现了返回为 nil , 并无其他的方法实现了 .

2.4.1 查找源码

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

2.4.2 官方文档

那么这时该怎么办 ? 不急 , 来找官方文档 .

小提示 :

主要看 Discussion

2.4.3 方法解析

文档说明了该方法的前世今生 , 总结如下 :

  • 1️⃣ : 该方法的目的 , 说白了就是你搞不定的方法 , 就交给别人处理 . 但是不能返回 self ,否则会一直找不到 , 陷入死循环 .
  • 2️⃣ : 如果不实现或者返回 nil ,会走到 forwardInvocation: 方法进行处理 .
  • 3️⃣ : 被转发消息的接受者参数和返回值等需要和原方法相同 .

也就是说我们可以自己新定义一个类 , 或者选一个现有的类 , 其实现了这个方法 , 就交由其去处理.

2.4.4 方法使用

写法如下 :

// 消息转发流程
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(saySomething)) {
        return [LBTeacher new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

在运行 , 就很愉快的打印出 LBTeachersaySomething 方法了 .

这个转发给单一方法实现对象的过程 , 我们也称之为 快速消息转发流程 .

2.5 慢速转发流程

当快速消息转发流程也并没有实现 , 或者返回为 nil , 就来到了 慢速消息转发流程 .

同样 , 根据我们的打印来寻求线索 .

2.5.1 查找源码

找到 methodSignatureForSelector .

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

查看源码同样是没有发现 . 老办法 , 找官方文档 .

2.5.2 官方文档

2.5.3 方法解析

其实熟悉方法和函数的同学应该很清楚 , 这个就是我们的方法签名 .

  • 该方法是让我们根据方法选择器 SEL 生成一个 NSMethodSignature 方法签名并返回 .
  • 这个方法签名里面其实就是封装了返回值类型,参数类型等信息。

2.5.4 方法使用

写法如下 :

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(saySomething)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

2.5.5 小提示

方法签名encode表

Code Meaning
c A char
i An int
s A short
l A long
l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

很明显 , 只给苹果一个方法签名是肯定不行的 , 必须要有方法实现啊 , 别急 , 刚刚我们在 forwardingTargetForSelector 的官方文档中看到 , 如果返回 nil 或者不实现 , 则会进入 forwardInvocation: 的流程 .

2.5.6 forwardInvocation

同样查找官方文档后 , 方法解析如下 :

  • 1️⃣ : forwardInvocationmethodSignatureForSelector 必须是同时存在的 .

  • 2️⃣ : 该方法可以自由指派多个对象来接收这个消息 .

  • 3️⃣ : 将消息发送到该对象。保存结果 ,运行时系统将提取此结果并将其传递给原始发消息人。

该方法可以指定多个转发者 , 而且由于 NSInvocation 的封装 , 可以自由调配 target , 参数等等 , 自由度较高 , 但与此同时花费也将更高 , 官方文档称其为 more expensive forwardInvocation: machinery . 使用方法如下 :

// 消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"%@",anInvocation);
    
    SEL aSelector = [anInvocation selector];
    
    if ([self respondsToSelector:aSelector]){
        [anInvocation invoke];
    }else if ([[LBTeacher new] respondsToSelector:aSelector]){
        [anInvocation invokeWithTarget:[LBTeacher new]];
    }
    else
       [super forwardInvocation:anInvocation];
}

至此 , 整个方法查找 , 动态方法解析 , 消息转发的完整流程我们已经讲完了 , 整个流程走完 , 如果仍然没有找到方法实现 , 那么苹果表示 我给过你太多机会了 , 我也没辙了.

3、面试题

最后 留下两道面试题 , 以帮助大家检验自己是否理解了 方法的本质 , 以及 方法查找和消息转发的知识 .

3.1 isKindOfClass 与 isMemberOfClass

#import "LBPerson.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     
        BOOL re3 = [(id)[LBPerson class] isKindOfClass:[LBPerson class]];       
        BOOL re4 = [(id)[LBPerson class] isMemberOfClass:[LBPerson class]];     
        NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];   
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     
        BOOL re7 = [(id)[LBPerson alloc] isKindOfClass:[LBPerson class]];       
        BOOL re8 = [(id)[LBPerson alloc] isMemberOfClass:[LBPerson class]];     
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}

LBPerson 为一个普通的 OC 类 .

问打印结果.

3.2 方法查找面试题

3.2.1 题目 1 :

LBStudent 继承自 LBPerson , LBPerson 中有一个方法如下 :

//声明
- (void)sayHi;

//实现
- (void)sayHi{
    NSLog(@"hi");
}

[LBStudent performSelector:@selector(sayHi)];

调用结果是什么 .

3.2.2 题目 2 :

LBPerson 继承自 NSObject , LBStudent 继承于 LBPerson , 项目中有一个 NSObject 的分类如下 :


// NSObject+LB.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (LB)
- (void)sayHi;
@end
NS_ASSUME_NONNULL_END

// NSObject+LB.m
#import "NSObject+LG.h"
#import <AppKit/AppKit.h>

@implementation NSObject (LB)
- (void)sayHi{
    NSLog(@"%s",__func__);
}
@end

问 :

[LBStudent performSelector:@selector(sayHi)];

打印结果是什么 .

如果将分类里 类方法改为实例方法 , 打印结果是什么 ?

大家可以在评论区留下自己的答案 , 下期揭晓 .

4、发散思维

大家可以由此来开拓思维 , 想一想如果让你来做一个防止崩溃的三方库 , 你会如何思考 , 如何设计 ?