iOS底层-动态方法决议

893 阅读7分钟

前言

在前面的文章中,我们讲述了方法的快速查找慢速查找过程,如果方法找不到会做什么呢,有没有挽救的机会呢?本文将对这些问题进行探究

案例之经典报错

  • 定义一个WSPerson类继承NSObject,然后定义一个WSTeacher类继承WSPerson
@interface WSPerson : NSObject

- (void)sayNB;      // 有实现
+ (void)sayCool;    // 无实现

- (void)sayLike;    // 未实现
@end

@interface WSTeacher : WSPerson

- (void)sayLearn;     // 有实现
+ (void)sayWrite;     // 有实现

- (void)sayLike;      // 未实现
@end

// 调用方法
WSTeacher *teacher = [[WSTeacher alloc] init];
[teacher sayLearn];  // WSTeacher 有实现的方法
[teacher sayNB];    // WSPerson 有实现的方法
[teacher sayLike];  // 都未实现的方法

结果可想而知:

截屏2021-07-07 09.23.28.png

  • 这个是我们常见的错误unrecognized selector sent to instance xxx,那么这个错误是怎么产生的呢,我再次去查看lookUpImpOrForward方法分析,我们在上节课中分析了,当父类为nil时,会进行一个赋值imp = forward_imp,再来看看 它是一个_objc_msgForward_impcache类型的指针,全局搜索下:

截屏2021-07-07 09.32.08.png

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache


ENTRY __objc_msgForward

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


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

  • 这里主要操作的是x17,由于TailCallFunctionPointer只是一个跳转,而上面__objc_forward_handler的值给x17,我们把重点放在__objc_forward_handler,通过查找发现它在c++代码中
__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就直接崩溃了,一点余地都不给,不给处理方法吗?显示是有余地的,下面我们继续去分析lookUpImpOrForward

动态方法决议

lookUpImpOrForward中循环查找时,如果没有找到,会走到resolveMethod_locked方法,也就是动态方法决议

if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    // 动态决议
    return resolveMethod_locked(inst, sel, cls, behavior);
}
  • behavior3LOOKUP_RESOLVER2
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
  • 得知behavior = LOOKUP_INITIALIZE | LOOKUP_INITIALIZE = 3
  • 所以可以得到 behavior & LOOKUP_RESOLVER = 3 & 2 = 2behavior ^= LOOKUP_RESOLVER = behavior = 3 ^ 2 = 1,这是behavior的值变成了1,如果再走到判断slowpath(behavior & LOOKUP_RESOLVER),这时behavior & LOOKUP_RESOLVER = 1 & 2 = 0不会进来,也就是如果没有新的behavior值,这个方法就只进来一次,相当于单例
  • 然后再来看看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);
}
  • 这里先判断是不是元类,如果不是是就走resolveInstanceMethod方法,如果是元类走resolveClassMethod
  • 判断走完后,系统会再次进行一次慢速查找,这整个方法也就是系统给的一次处理错误的机会
  • 如果都没有找到,会走lookUpImpOrForwardTryCache

resolveInstanceMethod

先来看看resolveInstanceMethod代码

代码分析

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:); // 系统发送的方法
    // 系统对resolveInstanceMethod方法进行了默认实现,所以这个if判断不会走进来
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); //发送到当前class,

    // 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); // 往当前类里面继续去查找一遍,如果发送成功了这里就可以找到imp了

    if (resolved  &&  PrintResolving) { 
    // 一些说明
    }
}
    1. 系统会将创建的@selector(resolveInstanceMethod:)方法,发送给当前class,这里一的resolve_sel一定会有值,因为系统给resolveInstanceMethod一个默认值NO,所以if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true)))这里面不会走进来
    1. 当前给系统发送完消息后,会走到lookUpImpOrNilTryCache方法
    IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
    {
      return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
    }
    
    这里通过behavior | LOOKUP_NILbehavior一个新值,再次调用lookUpImpOrForward时,如果循环没找到imp会走到那个单例子

代码验证

  • 通过分析我们得知,系统发送@selector(resolveInstanceMethod:)方法到class后,然后会往类里再查找一遍,那么我们在WSTeacher重写下这个方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel {

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

    return [super resolveInstanceMethod:sel];
}
  • 运行结果如下

截屏2021-07-07 16.23.10.png

运行的结果中resolveInstanceMethod打印是在报错之前,也就是说我们可以在里面做一些处理,例如动态添加方法。

防止报错

  • resolveInstanceMethod,当调用方法为sayLike时,调用其他方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if (sel == @selector(sayLike)) {
        IMP learnImp = class_getMethodImplementation(self, @selector(sayLearn));
        Method learnMethod = class_getInstanceMethod(self, @selector(sayLearn));
        const char *type = method_getTypeEncoding(learnMethod);
        return class_addMethod(self, sel, learnImp, type);
    }
    NSLog(@"resolveInstanceMethod: %@ - %@", self, NSStringFromSelector(sel));

    return [super resolveInstanceMethod:sel];
}

再来看下打印结果:

截屏2021-07-07 14.59.48.png

  • 如果找不到sayLike方法,就会走sayLearn方法
  • 我们在这个发送成功后,系统的bool resolved = msg(cls, resolve_sel, sel)这里的值就为true,然后再去class找方法就能找到了。完美解决了因没有实现而报错的问题。

resolveClassMethod

看完了对象方法的动态决议,再来看看类方法的动态决议

代码分析

if (! cls->isMetaClass()) {
} 
else {
     resolveClassMethod(inst, sel, cls);
     if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
         resolveInstanceMethod(inst, sel, cls);
     }
}


static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    // resolveClassMethod 有默认实现,不会在下面判断中直接return
    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;
    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);

    if (resolved  &&  PrintResolving) {
    // 打印说明
    }
}
    1. 这里基本和resolveInstanceMethod类似,只不过,这里是向元类发消息
    1. 处理完resolveClassMethod后,如果元类的Imp存在会查找一次resolveInstanceMethod方法。

代码验证

  • 再来在代码中测试下,在WSTeacher中重写resolveClassMethod,然后调用[WSTeacher sayCool]
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"resolveClassMethod: %@ - %@", self, NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

运行结果如下:

截屏2021-07-07 16.30.54.png

  • 说明类方法报错会走这里
  • 我们再处理下,当调用sayCool时,调用sayReadBook

防止报错

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(sayCool)) {
        IMP reakImp = class_getMethodImplementation(objc_getMetaClass("WSTeacher"), @selector(sayReadBook));
        Method readMethod = class_getInstanceMethod(objc_getMetaClass("WSTeacher"), @selector(sayReadBook));
        const char *type = method_getTypeEncoding(readMethod);
        return class_addMethod(objc_getMetaClass("WSTeacher"), sel, reakImp, type);
    }
    NSLog(@"resolveClassMethod: %@ - %@", self, NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

打印结果如下:

截屏2021-07-07 16.43.18.png

问题:
1. 这个里面为什么不用self
2. 对象方法要查找resolveInstanceMethod,为什么类的动态决议后也要查一遍?

  • 解答:
      1. 因为方法要加到元类,是给元类发消息,所以要写入WSTeacher的元类。
      1. 因为类方法以对象方法的形式存在元类中,而这个对象方法也可能是在元类的父类中继承过来的,所以类的动态决议要查两遍。

整合类和对象的动态决议

  • 通过分析我们知道对象方法都会走resolveInstanceMethod方法,类方法都会走resolveClassMethod方法,而类和元类最终都继承NSObject,子类无论是对象方法还是类方法都不用管,在NSObject都是对象方法。
  • 所以我们可以这样整合,先创建一个NSObject分类,再重写resolveInstanceMethod方法,然后添加相关处理:

截屏2021-07-07 17.51.30.png

最后为什么要return NO而不是[super resolveInstanceMethod:sel],是因为苹果底层默认返回的就是NO

  • 运行结果如下
    • 对象方法: 截屏2021-07-07 17.54.38.png
    • 类方法: 截屏2021-07-07 17.53.57.png

lookUpImpOrForwardTryCache

resolveInstanceMethodresolveClassMethod都没有找到时,会走lookUpImpOrForwardTryCache方法:

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

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); //behavior为第一次改变后的值
    }

    IMP imp = cache_getImp(cls, sel);
    ...
}

代码中会再次走lookUpImpOrForward,此时不会走进那个单例,直接返回nil

总结

    1. 当方法找不到时,都可以使用resolveInstanceMethod或者resolveClassMethod进行监听,并作出相应的补救处理。
    1. 我们可以利用这个特性,在实际项目中进行一些bug处理

辅助日志

除了动态决议,还有什么办法可以处理呢,显然还是有的,苹果提供了错误日志处理instrumentObjcMessageSends

截屏2021-07-07 18.37.01.png

  • 这里一个是对objcMsgLogEnabled进行赋值,一个是调用_objc_flush_caches方法进行操作,那么赋值的操作有什么用呢,我们进入看看:
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        // 日志文件路径
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
  • objcMsgLogEnabledtrue时,会走这个logMessageSend方法,里面有对日志的存储 我们先删除动态决议的代码,然后在脱离源码环境调用instrumentObjcMessageSends测试下:
extern void instrumentObjcMessageSends(BOOL flag);

// main
instrumentObjcMessageSends(YES);
[teacher sayLike];
instrumentObjcMessageSends(NO);

运行结果还是报错,然后cmd + shift + f进入/tmp/msgSends路径,找到一个msgSends-64056文件,然后打开:

截屏2021-07-07 18.57.00.png

  • 在文件中,可以看到调用的一些我们熟悉的方法,但出现新方法forwardingTargetForSelectormethodSignatureForSelector方法,这个就是消息转发中的方法,我们会在下篇文章去探究。