iOS 底层原理探索 之 动态方法决议

前言: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
复制代码

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa – 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa – 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa – 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程

前言

之前的文章中,我们探索了 runtime运行时 的消息发送 objc_msgSend 的底层原理流程 以及 消息的慢速查找流程的原理。 今天,我们从一个案例开始,重点探索下 runtime 的动态方法决议的过程。 好的, 下面就开始今天的探索。

开始

小案例

我们有一个 SMPerson 类, 其中定义了一些属性和方法,很简单:

image.png

其内部实现如下,同样也很简单:

image.png

此时,我们在平时开发过程中,难免会在其他地方,调用 SMPperson 的方法,很简单,如下:

image.png

接着,编译器会报错,一个十分经典的错误:

 SMObjcBuild[20894:318016] -[SMPerson likeGirl]: unrecognized selector sent to instance 0x1007c2a70
 SMObjcBuild[20894:318016] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SMPerson likeGirl]: unrecognized selector sent to instance 0x1007c2a70'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff2048387b __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00000001002fba80 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff2050638d -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff203eb90b ___forwarding___ + 1448
	4   CoreFoundation                      0x00007fff203eb2d8 _CF_forwarding_prep_0 + 120
	5   SMObjcBuild                         0x0000000100003a30 main + 64
	6   libdyld.dylib                       0x00007fff2032bf5d start + 1
	7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SMPerson likeGirl]: unrecognized selector sent to instance 0x1007c2a70'
复制代码

显然,是在告诉我们,SMPerson实例没有实现 likeGirl 这个方法, 找不到方法的实现,所以崩溃掉了。 为什么没有实现这个方法的话,会报这个错呢?
那么,这就要回忆一下上一篇关于 慢速查找流程lookUpImpOrForward 中 最开始 会定义一个 forward_imp :

const IMP forward_imp = (IMP)_objc_msgForward_impcache;
复制代码

在查找到最后,仍然没有查找到imp的时候

if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    // No implementation found, and method resolver didn't help.
    // Use forwarding.
    imp = forward_imp;
    break;
}
复制代码

会将 forward_imp 赋值给 imp 接着跳转出去,最终返回出去。所以,我们要看一下 上面 这个 _objc_msgForward_impcache 是一个什么样的imp
全局搜索之后,我们简略下寻找的过程,最终找到了这里:__objc_forward_handler, 在OBJC2 环境中 也就是

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

// 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);
}
复制代码

所以我们的方法找不到的时候,就会通过格式化的打印。

程序运行到这一步,显然是发生了错误,那么,我们是否可以做一些处理呢? 也很显然是可以的。也就是今天我们要探索的 动态方法决议的内容。

实例方法的动态决议

上面例子的 -[SMPerson likeGirl] 方法,显然是找不到了。
接下来,我们通过断点调试分析下,方法找不到了的流程在 lookUpImpForward 流程中的执行情况,会走到这个分支(这里是一个单利哦),

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
复制代码

之后来到, 执行到这里, 大致是这样,我们的方法是找不到的,按照一般的逻辑,应该就是要让程序奔溃掉,但是,这样会是系统不够稳定,用户的体验会不那么友好,那么,苹果在这里设计的时候,会再给我们一次机会来,拯救这次的方法无法找到的问题。只要我们在这里有做过处理,那么,系统会再去查找一次imp。

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // 不是元类,
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // 元类
        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);
}
复制代码

实例方法,会走到 resolveInstanceMethod 分支。

/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {

        return;
    }

    //系统调用 resolve_sel 方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 如果 在 cls 这个类里面 有 resolve_sel 这个方法处理
    bool resolved = msg(cls, resolve_sel, sel);

    // 继续去查找一遍
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) { ... }
}
复制代码

那么,我们现在在 SMPerson 类中实现一下 resolveInstanceMethod 方法,看会发生什么。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    NSLog(@"resolveInstanceMethod : %@-%@", self, NSStringFromSelector(sel));
    
    return [super resolveInstanceMethod:sel];
}
复制代码

日志如下:

SMObjcBuild[21877:375710] resolveInstanceMethod : SMPerson-likeGirl
SMObjcBuild[21877:375710] resolveInstanceMethod : SMPerson-likeGirl
SMObjcBuild[21877:375710] -[SMPerson likeGirl]: unrecognized selector sent to instance 0x100b98b20
复制代码

还是会崩溃,但是,会先执行,我们在 resolveInstanceMethod 方法中添加的内容。也就是说,我们可以在此处做处理,以避免程序崩溃。
此处有一个细节注意一下,就是 NSObject 默认是实现了 resolveInstanceMethod 方法的 也就是 系统兜底。

类方法的动态决议

在上一小节,元类的分支中,我们发现,通过实现元类的实例方法 resolveClassMethod ,我们是可以处理类方法没有实现的情况。
那么,我们知道,元类的实例方法就是类的类方法。所以我们在类中实现下类方法 resolveClassMethod 就可以了。

扩展

所有的实例方法没实现都会走到 resolveInstanceMethod 方法,所有的类方法没实现都会走到 resolveClassMethod 方法。那么,每个类都需要实现一遍很麻烦吗,我们其实可以新建一个 NSObject 的分类 中实现两个方法, 这样不至于每个类都需要写一次。

为什么要这样呢?

  • 苹果在设计系统的时候,当我们的方法找不到的时候,当我们实现方法动态决议的两个方法的时候,是给了我们机会来处理,不至于程序崩溃而导致的用户体验不友好。
  • 再就是,全局的方法没有实现,我们就可以在此处监听的到。
  • 全局处理一些模块的BUG,做日志来通知开发人员处理问题。
  • AOP 面向切面编程 (这里不展开讲了)

总结

动态方法决议的特性苹果在设计时,给我们提供了在方法找不到时,容错处理的一种可能。避免了对于用户的不友好体验,那么,如果动态方法决过程中我们没有做处理,下一个系统处理就会来到 消息转发流程。 下一篇,我们继续探索。大家加油!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享