iOS底层学习 - Runtime之方法消息的前世今生(二)

884 阅读9分钟

经过上一章的学习,我们了解到了方法是如何经历快速和慢速的流程进行查找的,如果经过方法的查找,可以找到对应IMP的话,则会直接返回,如果没有找到,就要进入本章的动态方法解析和消息转发流程了

通过上一章,我们基本了解了方法查找的3个阶段如下:

  • 消息发送阶段:从类及父类的方法缓存列表及方法列表查找方法;

    传送门☞iOS底层学习 - Runtime之方法消息的前世今生(一)

  • 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;

  • 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理;

通过阅读lookUpImpOrForward源码,我们知道动态方法解析,主要在_class_resolveMethod中,消息转发,主要在_objc_msgForward_impcache中,自此,我们来逐个进行研究

动态方法解析

lookUpImpOrForward方法中,经过对类,父类,元类的缓存和方法列表的查询后,仍旧没有找到方法,则会进入动态方法解析阶段,源码如下,我们可以看到,在经过 _class_resolveMethod后,会进行一遍retry操作,重新进行一遍方法的查找流程,并且只有一次动态方法解析的机会

动态解析主要方法为_class_resolveMethod,源码如下,主要是对元类的判断。因为类方法是储存在元类之中的,处理方式略有不同

  • 如果不是元类,则说明是对储存在类中的实例方法进行处理
  • 如果是元类,则说明是对元类中的类方法进行处理,但是元类中的方法是在根元类中以实例方法的形式存储的,所以最终会查找根源类的实例方法,调用实例方法解析查找
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);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

_class_resolveInstanceMethod

该方法是进行实例方法动态解析的主要实现方法,我们通过源码来逐行分析

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    ️✅// 判断系统是否实现SEL_resolveInstanceMethod方法,即+(BOOL)resolveInstanceMethod:(SEL)sel
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        ️✅// 如果没有找到(一般是不继承子NSObject的类),则直接返回
        return;
    }

    ️✅// 如果找到,则通过objc_msgSend调用一下+(BOOL)resolveInstanceMethod:(SEL)sel方法
    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 does not fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    ️✅// 同时再次寻找方法的IMP
    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));
        }
    }
}

通过阅读源码,我们知道,如果要进行方法的动态解析的话,则需要再系统方法

+ (BOOL)resolveInstanceMethod:(SEL)sel中进行处理,我们需要通过对未实现的方法指定一个已经实现的方法的IMP,并添加到类中,实现方法动态解析,这样我们就可以对一个未实现的方法进行动态解析了

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
     ️✅// 获取到需要动态解析的方法名
    if (sel == @selector(saySomething)) {
        NSLog(@"说话了");
         ️✅// 获取到需要动态解析到的方法sayHello的IMP和Method
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayHType = method_getTypeEncoding(sayHMethod);
         ️✅// 通过API添加方法
        return class_addMethod(self, sel, sayHIMP, sayHType);
    }
    
    return [super resolveInstanceMethod:sel];
}

_class_resolveClassMethod

如果是元类,则相关类方法的处理在_class_resolveClassMethod方法中处理,该方法的实现步骤和实例方法的实现步骤类似,只不过是消息发送的时候获取的是元类

当我们要进行类方法的动态解析时,需要添加+ (BOOL)resolveClassMethod:(SEL)sel进行动态方法解析

+ (BOOL)resolveClassMethod:(SEL)sel{
    
     if (sel == @selector(sayLove)) {
          ️✅// 获取到元类中存储的类方法sayObjc
         IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         Method sayHMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         const char *sayHType = method_getTypeEncoding(sayHMethod);
          ️✅// 将类方法实现添加在元类之中
         return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayHIMP, sayHType);
     }
     return [super resolveClassMethod:sel];
}

小结

  • 在方法经过缓存查找,方法列表查找后,后进入动态方法解析阶段
  • 动态方法解析分为实例方法和类方法的动态解析
  • 实例方法解析需要实现resolveInstanceMethod方法,并添加方法在类中
  • 类方法解析需要实现resolveClassMethod方法,并添加方法在元类中
  • 类方法存储在元类之中,如果没有实现相关类方法的动态解析,因为元类的方法以实例方法存储在根元类中,由于元类和根源类由系统创建,无法修改,所以可以再根元类的父类NSObject中,添加对应的实例方法resolveInstanceMethod进行动态解析
  • 由于动态方法解析依赖方法名等,统一处理起来耦合性较大,判断也比较多,所以在平时的运用较少

消息转发

lookUpImpOrForward方法中,经过对接受对象缓存,方法列表查找和动态方法解析后,如果以上步骤都没有进行处理,那么就会进入,消息处理的最后一步,即消息转发流程,这是补救方法崩溃最终的一步了,如果不想方法崩溃,那此时一定要处理了。

 imp = (IMP)_objc_msgForward_impcache;
 cache_fill(cls, sel, imp, inst);

_objc_msgForward_impcache也是一段汇编的代码,通过代码我们可以知道,汇编经过了一段__objc_msgForward方法,发现里面只有一段崩溃的实现,但是根据崩溃信息,我们可以发现中间还经过了___forwarding____CF_forwarding_prep_0等方法,但是是在CoreFoundation库中的,所以消息转发的处理在此时进行的

我们通过查看源码,发现在获取到IMP之后,系统会调用log_and_fill_cache,说明系统会对缓存的方法加日志,我们可以通过系统的日志在查看方法调用的情况。通过方法我们可以看到,日志会记录在/tmp/msgSends目录下,且通过objcMsgLogEnabled变量来控制是否存储日志

我们找到objcMsgLogEnabled的赋值是在instrumentObjcMessageSends之中,所以我们可以暴露这个方法,来达到外部打日志的操作
通过在代码中暴露instrumentObjcMessageSends方法,并定位在要崩溃的方法中,可以打上日志,来查看调用


extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGStudent *student = [LGStudent alloc] ;
        
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);

    }
    return 0;
}

通过寻找/tmp/msgSends文件,如下图所示,我们发现经过了resolveInstanceMethod,forwardingTargetForSelector,methodSignatureForSelector,doesNotRecognizeSelector,这就是我们要寻找的处理方法。

其中resolveInstanceMethod为方法动态解析,doesNotRecognizeSelector为经过上述的最后崩溃调用

所以,最终消息的转发就再forwardingTargetForSelectormethodSignatureForSelector中了,这也是一个快速慢速两种流程

快速流程 forwardingTargetForSelector

通过查看- (id)forwardingTargetForSelector:(SEL)aSelector方法的文档,我们可得

  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别人的对象进行相关方法的处理,但是不能返回self,否则会一直找不到
  • 该方法的效率较高,如果不实现或者nl,会走到forwardInvocation:方法进行处理
  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送
  • 被转发消息的接受者参数和返回值等需要和原方法相同

我们可以通过一下类似代码,处理相关方法,表示saySomething方法的处理被转发到LGTeacher的相关类中实现。至此消息转发的快速流程结束,不会崩溃

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

慢速流程methodSignatureForSelector

如果没有经过上述的消息转发的快速流程,那么会进入一个消息转发的慢速流程之中,实现慢速流程。

首先必须实现methodSignatureForSelector方法,通过下面文档的可以得出

  • 该方法是让我们根据方法选择器SEL生成一个NSMethodSignature方法签名并返回,这个方法签名里面其实就是封装了返回值类型,参数类型等信息。

然后实现methodSignatureForSelector后,还必须实现- (void)forwardInvocation:(NSInvocation *)anInvocation;方法进行处理,通过文档,我们可得

  • forwardInvocationmethodSignatureForSelector必须是同时存在的,底层会通过方法签名,生成一个NSInvocation,将其作为参数传递调用
  • 查找可以响应 InInvocation中编码的消息的对象。对于所有消息,此对象不必相同。
  • 使用 anInvocation将消息发送到该对象。anInvocation将保存结果,运行时系统将提取结果并将其传递给原始发送者。

我们可以看到NSInvocation源码

  • 封装了anInvocation.target -- 方法调用者
  • anInvocation.selector -- 方法名
  • - (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;-- 方法参数,

因此在此方法里面,我们可以决定将消息转发给谁(target),甚至还可以修改消息的参数,由于anInvocation会存储消息selector里面带来的参数,并且可以根据消息所对应的方法签名确定消息参数的个数,所以我们通过- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;可以对参数进行修改。总之你可以按照你的意愿,配置好anInvocation,然后执行[anInvocation invoke];即可完成消息的转发调用

我们可以通过如下类似的代码来实现消息的慢速转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);
   SEL aSelector = [anInvocation selector];

   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}

小结

  • forwardingTargetForSelector实现消息转发的快速流程,直接转发到能处理相关方法的对象中,并且方法要保持一致
  • methodSignatureForSelector提供一个方法签名,用来生成NSInvocation参数进行后续的使用
  • forwardInvocation通过对NSInvocation来实现消息的最终转发
  • 如果没有重写上述的方法,则进入NSObjectdoesNotRecognizeSelector方法,导致方法寻找不到,程序崩溃

消息源码

MJ大神提供的相关消息转发C语言实现源码

int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 调用 forwardingTargetForSelector:

    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
            NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}

流程图