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

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

经过上一章,咱们基本了解了方法查找的3个阶段以下:bash

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

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

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

  • 消息转发阶段:若是也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给能够处理消息的接受者来处理;编码

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

动态方法解析

lookUpImpOrForward方法中,通过对类,父类,元类的缓存和方法列表的查询后,仍旧没有找到方法,则会进入动态方法解析阶段,源码以下,咱们能够看到,在通过 _class_resolveMethod后,会进行一遍retry操做,从新进行一遍方法的查找流程,而且只有一次动态方法解析的机会 3d

动态解析主要方法为 _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)); } } } 复制代码

经过阅读源码,咱们知道,若是要进行方法的动态解析的话,则须要再系统方法code

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

流程图

相关文章
相关标签/搜索