iOS底层之Runtime探索(三)

15,981 阅读7分钟

序言

iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

iOS底层之Runtime探索(一)
iOS底层之Runtime探索(二)
前面的文章中讲到了objc_msgSend的方法查找过程,在通过lookUpImpOrForward方法的慢速查找没有找到imp后,就到了动态方法决议流程resolveMethod_locked

动态方法决议

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    // 元类判断
    if (! cls->isMetaClass()) { // 对象方法流程
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else { // 类方法流程
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
  • 判断当前的class是否为元类
  • 不是元类,走实力方法流程resolveInstanceMethod
  • 元类,走类方法流程resolveClassMethod,判断resolveClassMethod是否已解决问题,未解决再走resolveInstanceMethod流程;
  • 走完动态决议流程,重新找一次imp

resolveInstanceMethod方法解析

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 1.查询元类是否实现了resolveInstanceMethod:方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    // 2.发送消息resolveInstanceMethod:
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls

    // 3.再次查询sel对应的imp,resolveInstanceMethod可动态修改
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    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 {
            // Method resolver didn't add anything?
            _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));
        }
    }
}
  1. 定义resolveInstanceMethod:方法;
  2. lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true)):查询元类是否实现了resolveInstanceMethod:方法,cls->ISA(true)为类isa指向的元类,如果未实现直接返回;
  3. 通过objc_msgSend发送resolveInstanceMethod:消息,将返回结果给resolved
  4. lookUpImpOrNilTryCache(inst, sel, cls):再次查询当前类cls是否实现了sel对应的imp

lookUpImpOrNilTryCache方法解析

image.png

  • lookUpImpOrNilTryCache:是直接调用了_lookUpImpTryCachebehavior值为LOOKUP_NIL
  • _lookUpImpTryCache:的核心功能是再次走一次快速慢速方法查询。behavior值为LOOKUP_NIL,所以即使再次查不到也不会再走resolveMethod_locked流程。

resolveClassMethod方法解析

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    // 1.检查元类是否实现方法resolveClassMethod:
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    // 2.通过cls,获取处理的nonmeta
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }

    // 3.发送resolveClassMethod: 消息
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls

    // 4.再次查询imp是否实现
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    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 {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%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));
        }
    }
}
  1. 定义resolveClassMethod:方法;
  2. 通过方法lookUpImpOrNilTryCache,检查元类是否实现方法resolveClassMethod:,如果未实现直接返回;
  3. 通过cls,获取处理的nonmeta
  4. 通过objc_msgSend发送resolveClassMethod:消息,将返回结果给resolved
  5. lookUpImpOrNilTryCache(inst, sel, cls):再次查询当前元类cls是否实现了sel对应的imp

动态方法决议流程图

动态方法决议-导出(1).png

动态决议实例

定义一个类LGTeacher,声明两个方法,不在m文件实现。

  • 实例方法sayHelllo
  • 类方法sayByeBye

实现方法resolveInstanceMethod:但不做处理

image.png

实例方法

调用实例方法sayHello方法运行

image.png LGTeacher中的resolveInstanceMethod:方法成功进来,并且进来了两次,然后打开resolveInstanceMethod:中的注释代码运行。

image.png 运行成功,我们成功通过动态向类添加方法lg_instanceMethod处理了sayHello方法找不到,引起的崩溃问题。

类方法

调用类方法sayByeBye方法运行

image.png LGTeacher中的resolveClassMethod:方法成功进来,也是进来了两次,然后打开resolveClassMethod:中的注释代码运行。

image.png 运行成功,通过动态向元类添加方法lg_instanceMethod处理了sayByeBye方法找不到,引起的崩溃问题。

总结

我们可以在给公共父类NSObject添加分类,重写方法实现resolveInstanceMethod:,这样就可以成功解决所有imp找不到引起的崩溃问题了,同时我们也可以通过方法命名规范知道是哪个模块的哪个功能引起的问题,做BUG收集。
缺点:

  • 如果要对各个功能进行不同处理,需要大量业务功能逻辑判断,变得很复杂;
  • NSObject的分类中,很多系统的方法处理也会进来这里,会降低运行效率。

消息转发

如果我们未实现动态决议方法resolveInstanceMethod:,调用未实现的方法系统就会崩溃,这是我们通过bt指令查看堆栈信息 image.png 是通过doesNotRecognizeSelector:抛出错误的,在这之前还会经过_CF_forwarding_prep_0___forwarding___流程,这都是在CoreFoundation库里,苹果并没有真正开源CoreFoundation库。

image.png

在查阅苹果文档动态方法解析中,提到了消息转发 Message Forwarding,消息转发正是苹果给你提供的imp找不到的第二次机会。

Sending a message to an object that does not handle that message is an error. However, before announcing the error, the runtime system gives the receiving object a second chance to handle the message.

原理

  • 当对象发送的sel找不到imp时,系统会发送forwardInvocation:来通知该对象;
  • forwardInvocation:是从NSObject继承而来的,而在NSObject中只是调用doesNotRecognizeSelector:抛出错误;
  • 我们可以重写forwardInvocation:来实现消息的转发。
  • forwardInvocation:的唯一参数是NSInvocation
  • NSInvocation中,可以指定消息接收者target,调用invoke实现消息转发;
  • 消息转发完成,返回值将会返回给原始消息发送者;

文档里还介绍了有意思的点,在我们自己的类中要实现一个特定功能的方法,该功能已经在别的类中实现,我们又不能继承这个类,因为可能设计在不同的模块中,那我们就可以通过forwardInvocation:把消息转发特有的类去实现,这就实现类似多继承了。

image.png

实例

在关于forwardInvocation:api文档中,有个重要提示: image.png

除了forwardInvocation:之外,还必要重写methodSignatureForSelector:转发消息的机制使用从methodSignatureForSelector获得的信息来创建要转发的NSInvocation对象。重写方法必须为给定的选择器提供适当的方法签名方法签名可以是预先制定的,也可以是向另一个对象请求方法签名

LGTeacher中加上methodSignatureForSelectorforwardInvocation:方法后,再调用sayHello运行

image.png 成功运行,进入methodSignatureForSelectorforwardInvocation:方法,没有引起崩溃。
定义LGStudent实现sayHello方法,在LGTeacher中的forwardInvocation:,将消息转发给LGStudent

image.png

同样,在类方法的消息流程转发中,跟实例方法的实现是一致的,不过方法上要注意是+

image.png

forwarding​Target​For​Selector

在文档 forward​Invocation中还提供了forwardingTargetForSelector:方法

image.png

该方法的主要作用

无法识别的消息首先应指向的对象

如果对象实现(或继承)此方法,并返回非nil(和非self)结果,则返回的对象将用作新的接收方对象消息分派将恢复到该新对象。(显然,如果从该方法返回self,代码将陷入无限循环。)

如果您在非根类中实现此方法,如果您的类对于给定的选择器没有要返回的内容,那么您应该返回调用super实现的结果。

该方法为对象提供了一个机会,在更昂贵的forwardInvocation:机械接管之前重定向发送给它的未知消息。当您只想将消息重定向到另一个对象,并且可以比常规转发快一个数量级时,这非常有用。如果转发的目标是捕获NSInvocation,或在转发过程中操纵参数返回值,则此选项不适用

  • 大概意思是,如果只需要对未识别的消息,做简单的重定向,那么用此方法会比forwardInvocation:流程快很多很多,如果有额外的复杂操作,如捕获NSInvocation操纵参数返回值则不适用该方法。
  • forwardingTargetForSelector:的调用要在forwardInvocation:的前面。

image.png

forwardingTargetForSelector:实现重定向到LGStudent

image.png

流程图

KC消息转发机制.png

消息总结

到这里整个Runtime中对消息的处理流程objc_msgSend的解读都已完成,可能不够全面,中间有些细节难免有遗漏,不过重在整个流程的思维探索。

  1. 方法调用 --> objc_msgSend(id receiver, Sel)
  2. 通过isa获取类的地址cls
  3. 快速查找流程CacheLookup
  4. 慢速查找流程lookUpImpOrForward
  5. 动态方法解析resolveMethod_locked;
  6. 消息重定向forwardingTargetForSelector:;
  7. 消息转发forwardInvocation:

有兴趣的伙伴也可以搞一份源码跑一跑
iOS 全网最新objc4 可调式/编译源码

还有编译好的源码的下载地址