iOS进阶之路 (七)消息转发

680 阅读10分钟

上篇文章我们介绍到,OC方法底层通过objc_msgSend进行消息发送。依次进行缓存快速查找imp,类的方法列表慢速查找,如果仍然没有找到目标method,那么则进入消息转发流程。

1. 动态方法决议

1.1 测试代码

在父类AKPerson定义一个实例方法和一个类方法,都不实现。然后用子类AKStudent调用这两个方法。cmd + b 运行,程序崩溃'unrecognized selector sent to instance xxx'。

@interface AKPerson : NSObject

- (void)person_resolveInstanceMethed;
+ (void)person_resolveClassMethed;

@end

@implementation AKPerson
@end

@interface AKStudent : AKPerson
@end

@implementation AKStudent
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        AKStudent *student = [AKStudent alloc];
        [student person_resolveInstanceMethed];
        [AKStudent person_resolveClassMethed];
        
    }
    return 0;
}

根据我们上篇文章的内容,objc_msgsend(student, @selector(person_resolveInstanceMethed:))会在慢速流程中的lookUpImpOrForward方法中,遍历父类没有找到method,来到动态方法决议。

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方法内部,会根据当前类是否为元类,来判断当前所执行的方法是对象方法还是类方法。注意此时triedResolve = NO

/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
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);
        // 再次查找下方法,如果没有的话,就再转发一下resolveInstanceMethod方法
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

1.2 实例方法

/***********************************************************************
* _class_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 _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //发送SEL_resolveInstanceMethod消息,
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //再次查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了
    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 {
            // 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. lookUpImpOrNil检查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)方法。
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    // NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

我们的这个cls是没有这个方法的,但是其实根类NSObject已经实现了这个方法,默认返回NO。

// NSObject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
  1. 系统向当前AKPerson发送一个SEL_resolveInstanceMethod消息
  2. lookUpImpOrNil再次查找当前实例方法imp,找到就填充缓存,找不到就返回
  3. 结束动态方法决议,回到lookUpImpOrForward方法将triedResolve = YES并goto retry重新查找缓存和方法列表

动态方法决议的条件是 resolver && !triedResolver 。动态方法决议结束goto retry时,会将 triedResolve = YES,保证动态方法决议只有一次。

1.3 类方法

/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    // 查找下类是否实现了resolveClassMethod方法,NSObject类已经实现了
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 记住,此处是向元类发送resolveClassMethod消息,也就是调用resolveClassMethod方法
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_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 = 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 {
            // 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));
        }
    }
}

类方法的cls是元类。 类方法前3步与实例方法基本一致,不同的是在结束SEL_resolveClassMethod时,l ookUpImpOrNil会查找sel的imp,若有imp则退出动态方法决议,若无则进入_class_resolveInstanceMethod。

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呢?

类方法会以实例方法的形式存在元类中。这里的cls是元类,ins是类对象。在_class_resolveInstanceMethod会沿着isa走位图((类方法 -> [元类 - 根元类 - NSObject]))遍历查找有没有以实例方法形式存储的类方法。

1.4 动态方法决议

Objective C 提供了一种名为动态方法决议的手段,使得我们能够在运行时动态地为一个 selector 提供实现。我们仅仅要实现 +resolveInstanceMethod: 或 +resolveClassMethod: 方法,通过调用执行时函数 class_addMethod为指定的 selector 提供实现就可以。这两个方法都是 NSObject 中的类方法,其原型为:

+ (BOOL)resolveClassMethod:(SEL)name;
+ (BOOL)resolveInstanceMethod:(SEL)name;

实例方法

本类重写resolveInstanceMethod添加imp

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

@implementation LGStudent

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(doSomething)) {
        NSLog(@"%s - %@ 崩溃",__func__,NSStringFromSelector(sel));
        IMP resoloveIMP = class_getMethodImplementation(self, @selector(doResolve));
        Method resoloveMethod = class_getInstanceMethod(self, @selector(doResolve));
        const char *resoloveType = method_getTypeEncoding(resoloveMethod);
        return class_addMethod(self, sel, resoloveIMP, resoloveType);
    }
    return [super resolveInstanceMethod:sel];
}

- (void)doResolve {
    NSLog(@"reslove崩溃");
}

类方法

  • 类方法可以在本类重写resolveClassMethod往元类添加imp(实例方法存在类对象中,类方法存在元类对象中)
+ (BOOL)resolveClassMethod:(SEL)sel{
    // 本类重写ClassMethod
     if (sel == @selector(doNOImpClassMethod)) {
         NSLog(@"%s - %@ 崩溃",__func__,NSStringFromSelector(sel));
         IMP classIMP = class_getMethodImplementation(objc_getMetaClass(@"AKPerson"), @selector(doResolve));
         Method classMethod = class_getInstanceMethod(objc_getMetaClass(@"AKPerson"), @selector(doResolve));
         const char *classType = method_getTypeEncoding(classMethod);
        // 类方法在元类 objc_getMetaClass("AKPerson")
        return class_addMethod(objc_getMetaClass("AKPerson"), objc_getMetaClass(@"AKPerson"), classIMP, classType);
     }
     return [super resolveClassMethod:sel];
}
  • 或者在NSObject分类重写resolveInstanceMethod添加imp(元类的方法以实例方法存储在根元类中,由于元类和根源类由系统创建无法修改,所以只能在根元类的父类NSObject中)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    // NSObject分类重写InstanceMethod
     if (sel == @selector(sayLove)) {
         NSLog(@"%s - %@ 崩溃",__func__,NSStringFromSelector(sel));
         IMP classIMP = class_getMethodImplementation(self, @selector(doResolve));
         Method classMethod = class_getInstanceMethod(self, @selector(doResolve));
         const char *classType = method_getTypeEncoding(classMethod);
        // 类方法在元类 objc_getMetaClass("NSObject")
        return class_addMethod(objc_getMetaClass("NSObject"), sel, classIMP, classType);
     }
     return [super resolveClassMethod:sel];
}

总结

  • 实例方法可以重写resolveInstanceMethod添加imp
  • 类方法可以在本类重写resolveClassMethod往元类添加imp,或者在NSObject分类重写resolveInstanceMethod添加imp
  • 动态方法解析只要在任意一步lookUpImpOrNil查找到imp就不会查找下去(本类做了动态方法决议,不会走到NSObjct分类的动态方法决议)

问题来了,把所有崩溃都在NSObjct分类中处理,用前缀区分业务逻辑,是不是一劳永逸了?
这样处理耦合度很高,要做大量的判断。又或者有人已经在在NSObject之前的类已经做了动态决议。而且SDK封装起来也需要容错率。如何解决请看下面的消息转发机制。

2. 消息快速转发

当我们在动态决议阶段不做任何处理的话,如果我们调用一个不存在的方法的时候,程序会崩溃。在崩溃信息中,崩溃之前底层还调用了forwarding和_CF_forwarding_prep_0等方法。

  1. 在lookupimporforward方法查找流程里,当方法找到的时候都会调用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);
}
  1. 日志会记录在/tmp/msgSends目录下,并且通过objcMsgLogEnabled变量来控制是否存储日志
bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char    buf[ 1024 ];

    // Create/open the log file--创建log日志文件
    if (objcMsgLogFD == (-1))//此处为YES
    {   //此处我们看到有个文件路径,猜测是日志的文件路径
        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;
}
  1. 全局搜索objcMsgLogEnabled,发现instrumentObjcMessageSends可以改变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;
}

  1. 我们可以根据以下代码来记录并查看日志
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        AKStudent *student = [[AKStudent alloc] init];
        
        instrumentObjcMessageSends(true);
        [student doNoImpMethod];
        instrumentObjcMessageSends(false);
    }
}

访问msgSends文件

在动态方法决议和doesNotRecognizeSelector崩溃之间,就是消息转发流程:快速流程forwardingTargetForSelector和慢速流程methodSignatureForSelector。

2.1 快速转发 - forwardingTargetForSelector

forwardingTargetForSelector在源码中只有一个声明,在方法文档中找到该方法的具体解释。

forwardingTargetForSelector会返回一个对象,将自己处理不了的消息转发给该对象,询问该对象是否能够处理该消息。

- (id)forwardingTargetForSelector:(SEL)sel {

    if ([AKTeacher respondsToSelector:sel]) {
        return AKTeacher.class;
    }
    return nil;
}

@end

2.2 慢速转发 - methodSignatureForSelector

如果快速转发还不能找到method,会来到慢速转发methodSignatureForSelector。

1. -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

  • methodSignatureForSelector会返回一个方法签名NSMethodSignature
  • 根据NSMethodSignature创建NSInvocation对象。
  • 将NSInvocation对象作为参数传给forwardInvocation方法.

2. -(void)forwardInvocation:(NSInvocation *)anInvocation

  1. forwardInvocation方法类似于将消息当做事务堆放起来,方法内部将消息给能处理该消息的对象,就算不操作也不会崩溃,这里也是防崩溃的最后处理机会。
+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
  1. 异常是在doesNotRecognizeSelector方法里面抛出的,所以我们重写forwardInvocation方法后,如果不在里面执行父类的方法,程序是不会崩溃的
// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}
  1. 慢速转发补救
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (NSSelectorFromString(@"saySomething") == aSelector) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 把不能够处理的方法,返回一个方法签名
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 给不同的方法,传递给不同的能够处理的类去处理
    SEL aSelector = [anInvocation selector];
    if ([[AKTeacher alloc] respondsToSelector:aSelector]) {
         // 此时 AKTeacher 能够处理 方法 aSelector
          [anInvocation invokeWithTarget:[AKTeacher alloc]];
    }
    else {
          [super forwardInvocation:anInvocation];
    }
}

3. 消息转发总结

当开发者调用了未实现的方法,苹果提供了三个解决途径:

3.1 动态方法决议:Method resolution

  1. runtime调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。
  2. 如果你添加了函数并返回YES, 那运行时系统就会重启一次消息发送<>lookupimporforward的过程。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

3.2 快速转发:Fast forwarding

  1. 如果目标对象实现了-forwardingTargetForSelector:,runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
  2. 只要这个其他对象不是nii或者self,那运行时系统就会重启一次消息发送的过程。此时,发送消息的对象变成你返回的那个其他对象。
- (id)forwardingTargetForSelector:(SEL)aSelector;

这里叫fast,只是为了区别下一步的慢速转发。因为这一步不会创建新的对象,而Normal forwarding会创建一个NSInvocation对象。

3.3 慢速转发:Normal forwarding

这一步是runtime最后一次给你挽救的机会。

  1. 先调用-methodSignatureForSelector:获取方法签名并创建NSInvocation对象
  2. 将NSInvocation对象作为参数传给-forwardInvocation方法,在其内部做消息处理。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;