iOS底层探究-----消息动态决议

1,148 阅读14分钟

前言

在开发过程中,有这么一个现象,如果在类里面调用了不存在的方法,或者是调用了未实现的方法,就会报错,如:...unrecognized selector sent to instance...。作为一名开发者,总是对不知道的东西充满神秘感和保持好奇心。就是,为什么调用不存在的方法会报这样的错误了?接下来,就以这个点为突破口,进行探究,会让童鞋们发现不一样的精彩。

资源准备

进入主题

当类调用到未找到的方法,就报未找到该方法的错误,既然是方法查找,再根据上篇文章方法慢速查找流程,那么就是直接定位到objc底层lookUpImpOrForward()方法上了。当本类以及本类的所有父类里面,都找不到对应方法的时候,就会返回一个forward_imp,那么故事就从这里开始了。

方法找不到报错的底层原理

方法找不到时,返回的impforward_imp

因为在上一篇文章里面,详细的分析了这块源码,所以这里就不再把源码全部贴出来。只把在这篇文章分析所需要的源码给贴出来。根据源码中的注释,方法慢速查找流程的个步骤:①、②、③、④查看源码

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
   
//-------------------代码省略..............
    
    for (unsigned attempts = unreasonableClassCount();;) {
//---- ①、先在缓存中查找一次,因为是为了防止所查找的方法,在类初始化的时候,就已经加入缓存中了,所以先找一遍以防万一
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            Method meth = getMethodNoSuper_nolock(curClass, sel);//---- ②、通过二分法查找,获取当前方法
            if (meth) {//找到了方法
                imp = meth->imp(false);
                goto done;//写入缓存
            }
             //---- ④、如果所有的父类都找完了,还是没有的话,会返回一个外传的imp
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
//----③、如果当前子类找不到,就直接到父类里面找,父类里面查找也分为快速和慢速两个流程.
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            break;
        }
        if (fastpath(imp)) {
            goto done;//缓存填充
        }
    }
//-------------------------- 代码省略...............
    return imp;
}

由于在本类以及本类的所有父类里面,都找不到对应方法,所以imp返回了一个forward_imp,而根据源码,forward_imp的定义就是_objc_msgForward_impcache

forward_imp的定义进行跟踪

那么全文索引_objc_msgForward_impcache,查看他的实现,继续找线索 (经验提示:一般带下划线的,大概率找汇编)。真机环境---arm64C546C74B-1B80-4EA0-8B4C-24E9C4CED671.png 此时,我们通过源码看到,在_objc_msgForward_impcache里面只执行跳转__objc_msgForward的方法,那么再看这个跳转的方法。

__objc_msgForward的方法中,汇编指令adrp表示,将以页为单位__objc_forward_handler的地址取到 x17寄存器里面,而ldr指令是将__objc_forward_handler地址所指向的值赋给x17寄存器,最后再执行TailCallFunctionPointer x17

而我们再TailCallFunctionPointer的宏定义:

.macro TailCallFunctionPointer
	// $0 = function pointer value
	braaz	$0
.endmacro

发现,只是跳转到$0,带到源码里面执行,就是要跳转到x17寄存器,而x17的地址就是__objc_forward_handler,也就是接下来的线索。

forward_imp最终指向objc_defaultForwardHandler()

当全文索引__objc_forward_handler时,没有找到他的实现,那么证明此方法不再是汇编方法,而是C++方法,去掉头部下划线,直接搜索objc_forward_handler。最终在objc-runtime.mm文件里面找到

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;

objc_defaultForwardHandler作为值,传递给*_objc_forward_handler,当看到值objc_defaultForwardHandler的实现时,是不是有种熟悉的感觉了,我们调用不存在的方法的错,是不是就是这样的格式。class_isMetaClass(object_getClass(self))判断是类方法还是实例方法,打印了类名object_getClassName(self),还打印了方法名sel_getName(sel),以及类地址打印self

报错底层原理小结

  • 当方法在本类以及本类的所有父类里面,都找不到的时候,imp返回的是forward_imp,而forward_imp的定义是_objc_msgForward_impcache

  • _objc_msgForward_impcache跳转到__objc_msgForward,而__objc_msgForward返回到C++方法objc_forward_handler();

  • objc_forward_handler()objc_defaultForwardHandler传值,在objc_defaultForwardHandler里面,就是报错打印的全部信息。

到了这里,我们就很清晰的知道这个报错的底层原理,我们知道当前的方法发生了错误,当错误找到之后,就不能进行其他的处理了吗?

既然提出来了,显然是有处理的方式方法的,而处理的方式方法,就涉及到一个重要的点----消息的处理流程

方法动态决议---对象方法动态决议

先做些基础建设,在objc源码中,创建一个LGPerson类,再创建一个LGTeacher类继承于LGPerson,如下面源码展示:

//创建一个LGPerson类
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject
- (void)say666;
@end

#import "LGPerson.h"
@implementation LGPerson
@end

//创建一个LGTeacher类
#import "LGPerson.h"
@interface LGTeacher : LGPerson
@end

#import "LGPerson.h"
@implementation LGPerson
@end

然后在main.m中初始化LGTeacher类,并调用父类LGPersonsay666方法。 BBC3EED9-B8B6-4374-9428-0B82D72D8E8D.png 当执行到断点处时,再到底层方法查找函数lookUpImpOrForward()中再打上断点。再跳过main.m里面的那个断点,就会执行到lookUpImpOrForward()中的断点处,然后再进行lldb调试,确认当前的类是不是LGTeacher45388DAD-8CE8-4158-8A9A-DADE51FAAB89.png

确定了是LGTeacher类后,再在下图处进行断点,查看下imp的内容: 440FC480-B968-4160-9356-E9265AC638A7.png 通过lldb调试,发现imp中是空的。断点这块代码是一个单利(详情去看:补充1),只能执行一次。

impnil时,给了一次修正机会

执行到这个单利里面后,进入resolveMethod_locked()方法里面:

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);
        }
    }
 
 // ---- ①
 
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

因为此时,imp是为nil的,那么按照常规流程走下去,程序就直接奔溃了。然而,苹果系统开发设计者,认为这样直接奔溃不太友善,对用户的体验也不怎么好。所以,就给iOS开发者一次拯救自己代码的机会(在①注释之间)。也就意味着,在接下来能够对imp进行处理,能给到一个不为空的imp返回出去,这样就能保证程序不再奔溃。

当有了一个不为空的imp时,通过lookUpImpOrForwardTryCache()方法返回出去。继续跟踪去查看是如何返回的:进入lookUpImpOrForwardTryCache() -->_lookUpImpTryCache()_lookUpImpTryCache()源码实现:

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
//---- 判断时候初始化
    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
//---- 查找父类有没有
    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
//---- 查找共享缓存有没有
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
//---- 当imp为NULL时,再次查找一次
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
  • 也就是说,当impnil时,只要我们把imp进行处理了,那么系统会再次帮我们进行查找一次。虽然说是会消耗一些性能。

查找不到实例方法,动态处理imp

根据上文中,处理imp的代码块(在①注释之间),当前我们是在LGTeacher类中,并不是元类,所以进入if判断里面,进入resolveInstanceMethod()方法。查看其实现:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
//----- resolve_sel消息里,需要实现的方法
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
//---- 系统自动发送resolve_sel消息给开发者,指明了类和方法名,只要实现了resolve_sel消息里的方法,就不会再报错
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//---- 判断resolveInstanceMethod是否执行成功    
    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
    
//---- 成功之后,然后就到当前的表里面,继续去查找一遍
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    //。。。。。。。。
}
  • 也就是说,当系统检测到查找的impnil时,系统自动发送resolve_sel消息给开发者,只要实现了resolve_sel消息里的方法,就不会再报错,而实现的方法就是resolveInstanceMethod:

  • 当判断成功执行resolveInstanceMethod:方法之后,就执行lookUpImpOrNilTryCache方法,继续去再次查找一遍。

当然,就算不实现resolveInstanceMethod:方法,也不会执行if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true)))这个判断,因为,既要系统去查找resolveInstanceMethod方法,如果不是实现,系统自己又要去报错,那么会造成系统的更加不稳定。所以系统对resolveInstanceMethod方法默认的设置:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

知道需要实现resolveInstanceMethod方法后,那么此时,就可以在LGTeacher类里面,创建一个sayNB的方法,调用resolveInstanceMethod:方法。动态添加一个imp

#import "LGPerson.h"
@interface LGTeacher : LGPerson
- (void)sayNB;
@end

#import "LGTeacher.h"
#import <objc/message.h>

@implementation LGTeacher

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

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    // 处理 sel -> imp
    if (sel == @selector(say666)) {
        IMP sayNBImp     = class_getMethodImplementation(self, @selector(sayNB));
        Method method    = class_getInstanceMethod(self, @selector(sayNB));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayNBImp, type);
    }
    
    NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));

    return [super resolveInstanceMethod:sel];
}
@end

调用resolveInstanceMethod方法,既然没有say666的方法,那么就有创建实现好的sayNB的方法替换上去。运行后,没有再次报错。那么就处理成功了。 9B3D7FEC-98FE-41C6-8BB6-2C3D6DED9666.png

  • 注:在调用resolveInstanceMethod方法时, NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));会执行两次,为什么会这样,可以直接去看:补充2的详情。

对象方法动态决议小结

  • 当查找不到某对象方法时,lookUpImpOrForward()返回impnil,然后就进入到一个单利的判断里面,执行resolveMethod_locked();

  • resolveMethod_locked()里面,可以知道系统给了处理imp的机会,只要对imp进行动态处理,能给到一个不为空的imp返回出去,那么系统会再次帮我们进行查找一次,这样就能保证程序不再奔溃。

  • 动态处理imp的流程是,在类里面调用resolveInstanceMethod方法,把空的imp传一个已经存在的方法的imp,使之不在为空,相当于用这个已存在的方法进行替换。这样就完成了imp的动态处理。

  • 当判断完resolveInstanceMethod正确执行后,bool resolved,当执行成功之后,返回到lookUpImpOrNilTryCache()里面,再次查找,有了imp,就能找到并返回对应方法。

方法动态决议---类方法的动态决议

resolveMethod_locked()方法里面,当不是在元类的时候,处理imp是进入if的判断执行的,那么当在元类的时候了,处理imp应该是进入else判断了。接下来,在LGPerson类里面再创建类方法:

#import <Foundation/Foundation.h>
@interface LGPerson : NSObject
- (void)say666;
+ (void)sayHappy;
@end

//在main.m里面调用
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [LGTeacher sayHappy];
    }
    return 0;
}

最后还是来到else的判断里面; 61029FA3-2547-4AB9-A529-61DC86153A80.png

查找不到类方法,动态处理imp

接着就进入到resolveClassMethod()方法里面,查看其实现,可以发现,和我们刚刚分析的实例方法动态决议很相似:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
//---- 判断在执行lookUpImpOrNilTryCache,有没有实现resolveClassMethod方法
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }
//---- 对元类进行局部操作,防止其未实现
    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);
        }
    }
//---- 是否已经发送消息    
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//---- 是否已经执行resolveClassMethod,但是resolveClassMethod是执行在类里面的,因为在类里面执行类方法,就等同于在元类里面执行实例方法
    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
//---- 成功之后,然后就到当前的表里面,继续去查找一遍
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    //。。。。。。。。。。。。
}
  • 那么resolveClassMethod()方法,也就是多了对元类进行局部操作,防止其未实现,剩下的,也是当系统检测到查找的impnil时,系统自动发送resolve_sel消息给开发者,然后就需要实现resolveInstanceMethod:方法。

  • 当判断成功执行resolveInstanceMethod:方法之后,就执行lookUpImpOrNilTryCache方法,继续去再次查找一遍。

  • 这里resolveClassMethod是执行在里面的,因为在里面执行类方法,就等同于在元类里面执行实例方法

那么接下来,就是在LGTeacher类里面实现resolveClassMethod方法了。

#import "LGTeacher.h"
#import <objc/message.h>

@implementation LGTeacher

+ (void)sayScott{
    NSLog(@"%@ - %s",self , __func__);
}

//元类的以对象方法的方法
+ (BOOL)resolveClassMethod:(SEL)sel{
   
    NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));
    
    if (sel == @selector(sayHappy)) {
        IMP sayNBImp     = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayScott));
        Method method    = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayScott));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type);
    }

    return [super resolveClassMethod:sel];
}
@end

和类方法的处理方式最大的不同是,不是从self里面获取,而是从objc_getMetaClass("LGTeacher")元类里面获取。运行结果: 37FEF8B2-1D51-46E9-B510-822857D660D8.png 执行了sayScott方法,也就是imp处理成功。

再次执行resolveInstanceMethod的原因

else判断里面,执行了resolveClassMethod方法后,接下来还得执行一个关于inst是否存在的判断if (!lookUpImpOrNilTryCache(inst, sel, cls)),最后再执行了一次实例方法动态处理imp的方法resolveInstanceMethod,这是为什么了?

  • 因为,这是为了判断当前的类里面,是否存在resolveInstanceMethod方法。 根据isa的走位图(详情见:补充2),可以知道,类里面的类方法,在元类里面是以实例方法存储的。那么在类里面是以类方法通过resolveClassMethod方法动态处理imp,同时还可以在元类里面,以实例方法通过resolveInstanceMethod方法动态处理imp。这就是为什么要再次执行resolveInstanceMethod方法的原因。

类方法动态决议小结

  • 当查找不到某类方法时,lookUpImpOrForward()返回impnil,然后就进入到一个单利的判断里面,执行resolveMethod_locked();

  • resolveMethod_locked()里面,执行else判断部分,进行动态处理imp,处理的操作,首先是对类里面的类方法进行查询,执行resolveClassMethod方法,当完成imp的动态处理之后,还要进行元类的查询处理。

  • 因为类里面执行类方法,就等同于在元类里面执行实例方法,所以判断inst是否存在,如果存在,就要再次调用resolveInstanceMethod方法,在元类里面再处理一次。

  • 当判断完resolveInstanceMethod正确执行后,bool resolved,当执行成功之后,返回到lookUpImpOrNilTryCache()里面,再次查找,有了imp,就能找到并返回对应方法。

NSObject分类囊括实例方法类方法的动态处理imp

既然在处理实例方法和类方法,都是要用到这个,而NSObject类是跟父类,那么直接就可以用NSObject的分类来实现:

#import "NSObject+LG.h"
#import <objc/message.h>

@implementation NSObject (LG)

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

+ (void)sayScott{
    NSLog(@"%@ - %s",self , __func__);
}

#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"


+ (BOOL)resolveInstanceMethod:(SEL)sel{

    NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));

    if (sel == @selector(say666)) {
        IMP sayNBImp     = class_getMethodImplementation(self, @selector(sayNB));
        Method method    = class_getInstanceMethod(self, @selector(sayNB));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayNBImp, type);
    }else  if (sel == @selector(sayHappy)) {
        IMP sayNBImp     = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayScott));
        Method method    = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayScott));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type);
    }
    return NO;
}
@end

这样就能一步到位了。

补充

补充1、位运算单利的计算过程

为什么说一个单利了?

behavior是由lookUpImpOrForward()传入的,根据其底层汇编,知道,behavior = 36D4CF6E5-2C44-4599-B864-14CE7E723619.png 再来看LOOKUP_RESOLVER,是个枚举值,LOOKUP_RESOLVER = 2BC08012F-AC28-4242-9865-80467B26A7FC.png 带入到if判断里面去运算,behavior = behavior & LOOKUP_RESOLVER = 3 & 2 = 2,接着再进入判断里面运算,behavior ^= LOOKUP_RESOLVER 转化下就behavior = behavior ^ LOOKUP_RESOLVER = 2 ^ 2 = 0。当behavior = 0之后,下次再执行这个判断时,behavior & 任何数,都是为0的,所以,就再也进入不了判断里面,也就是只执行了一次。behavior标记当前的行为是LOOKUP_RESOLVER

behavior的传值为线索,进入resolveMethod_locked(),再进入lookUpImpOrForwardTryCache(),会看到

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
//---- 此处可以看到,behavior | LOOKUP_NIL是再次动态默认赋值,意味着当前是一个新的操作,behavior的行为被标记为LOOKUP_NIL
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
  • 这个位运算的判断,就是一个标记的过程

补充2、执行两次的原因

在调用say666方法时,找不到该实例方法,动态处理imp时,执行resolveInstanceMethod方法时,打印了两次。 28C25858-258D-4F2A-81AA-4EEF147987C6.png 探究这个问题就用到了汇编,在resolveInstanceMethod方法里面打上断点,执行程序,当第一次执行到断点处时,也是第一次打印say666方法,如下图: 7516C287-52A7-4B6C-BB2D-3F8054CC535C.png

  • 通过汇编bt指令,查看堆栈信息,可以看出,第一次执行的流程,就是我们上面分析的流程。先慢速查找lookUpImpOrForward --> resolveMethod_locked(消息动态决议) --> resolveInstanceMethod

接下来就是跳过断点,进行第二次打印,通过汇编bt指令,查看堆栈信息: B81F9231-966B-4193-BCFC-D76D1AA14987.png 通过堆栈信息,可以看出,在执行第二次的时候,多了个消息转发流程(文章:消息转发)。 由底层系统库CoreFoundation库调起,在消息转发完成以后,再进行慢速查找流程,接着再进入动态方法决议,又调用一次resolveInstanceMethod方法,所以总共是两次

或者我们可以直接实现这些流程的方法,看运行后的结果,也是能知道这些方法的调用流程的: 58176894-BBFE-44AA-AB68-51528D632934.png 从结果来看,执行的顺序还是:resolveInstanceMethod --> forwardingTargetForSelector --> methodSignatureForSelector --> resolveInstanceMethod. 最终,还是打印了两次。