AI智能
改变未来

iOS 方法动态决议和消息转发机制

iOS 方法动态决议和消息转发机制

  • 1. 动态方法决议
  • 1.1 实例方法动态解析 _class_resolveInstanceMethod
  • 1.2 _class_resolveClassMethod
  • 小结
  • 2. 消息转发
    • 2.1 快速转发流程
    • 2.2 慢速转发流程
  • 总结
  • 通过上一篇章的学习,OC调用方法,底层是调用 objc_msgSend 发送消息。在发送消息时会经过一系列的快速查找、慢速查找,如果查找到对应的 IMP,直接返回;如果没有找到,就会进入到方法的动态解析和消息转发流程。

    1. 动态方法决议

    通过探索

    objc_msgSend

    源码,当慢速查找依然没有找到

    IMP

    时,会进入方法动态解析阶段,源码如下:


    在经过

    _class_resolveMethod

    方法后,在进行一次

    retry

    ,重新进行一遍方法的查找流程,而只有一次动态方法解析的机会就是在

    _class_resolveMethod

    方法中。

    _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_resolveMethod

    中的处理有所不同

    元  类:说明是对元类中的类方法进行处理,但是元类中的方法是在根元类中以实例方法的形式存储的,所以最终会查找根元类的实例方法,调用实例方法解析查找。非元类:对储存在类中的实例方法进行处理。

    1.1 实例方法动态解析 _class_resolveInstanceMethod

    _class_resolveInstanceMethod

    方法中对实例方法动态解析,源码如下:

    static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst){// 1. 判断系统是否实现SEL_resolveInstanceMethod方法// 即+(BOOL)resolveInstanceMethod:(SEL)sel,// 继承自NSObject的类,默认实现,返回NOif (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,NO/*initialize*/, YES/*cache*/, NO/*resolver*/)){// Resolver not implemented.// 不是NSObject的子类,也未实现+(BOOL)resolveInstanceMethod:(SEL)sel,// 直接返回,没有动态解析的必要return;}// 2. 系统给你一次机会 - 你要不要针对 sel 来操作一下下BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);// 3. 再次寻找IMPIMP 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{NSLog(@\"来了老弟:%s - %@\",__func__,NSStringFromSelector(sel));if (sel == @selector(saySomething)) {NSLog(@\"说话了\");IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));const char *sayHType = method_getTypeEncoding(sayHMethod);return class_addMethod(self, sel, sayHIMP, sayHType);}return [super resolveInstanceMethod:sel];}

    我们也可以在此方法中根据方法的前缀、路由、事务,跳转的不同的页面,进行bug收集。

    1.2 _class_resolveClassMethod

    如果是元类,在

    _class_resolveClassMethod

    方法中对相关类方法的进行动态解析,该方法的实现步骤和实例方法的实现步骤类似,区别是消息发送的时候获取的是元类,即:


    _class_resolveClassMethod

    源码如下:

    static void _class_resolveClassMethod(Class cls, SEL sel, id inst){assert(cls->isMetaClass());// 1. 判断系统是否实现SEL_resolveClassMethod方法// 即+(BOOL)resolveClassMethod:(SEL)sel,// 继承自NSObject的类,默认实现,返回NOif (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,NO/*initialize*/, YES/*cache*/, NO/*resolver*/)){// Resolver not implemented.return;}//2. 系统给你一次机会//   通过objc_msgSend调用一下+(BOOL)resolveClassMethod:(SEL)sel方法BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;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//3. 再次查找IMPIMP 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));}}}

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

    + (BOOL)resolveClassMethod:(SEL)sel{NSLog(@\"来了类方法:%s - %@\",__func__,NSStringFromSelector(sel));if (sel == @selector(sayLove)) {NSLog(@\"说- 说你你爱我\");IMP sayHIMP = class_getMethodImplementation(self, @selector(sayObjc));Method sayHMethod = class_getClassMethod(self, @selector(sayObjc));const char *sayHType = method_getTypeEncoding(sayHMethod);// 类方法在元类 objc_getMetaClass(\"LGStudent\")return class_addMethod(self, sel, sayHIMP, sayHType);}return [super resolveClassMethod:sel];}

    小结

    1. 方法先经过缓存查找,方法列表查找后,后进入动态方法解析阶段
    2. 实例方法解析需要实现
      resolveInstanceMethod

      方法

    3. 类方法解析需要实现
      resolveClassMethod

      方法

    4. 类方法存储在元类之中,由于
      元类

      继承自

      根元类

      根元类

      最终继承自

      NSObject

      ,因此对类方法解析的时候,最终会查找到

      NSObject

      。由于元类和根源类由系统创建,无法修改,所以可以再根元类的父类

      NSObject

      中,添加对应的实例方法

      resolveInstanceMethod

      进行动态解析。

    2. 消息转发

    在方法查找过程中,经过缓存查找,方法列表查找和动态方法解析,如果以上步骤都没有查找到

    IMP

    ,也没有进行方法动态解析,那么就会进入最后一步,崩溃。

    _objc_msgForward_impcache

    是汇编方法,如下:

    STATIC_ENTRY __objc_msgForward_impcache// Method cache version// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band Z is 0 (EQ) for normal, 1 (NE) for stretbeq	__objc_msgForwardb	__objc_msgForward_stretEND_ENTRY __objc_msgForward_impcacheENTRY __objc_msgForward// Non-stret versionMI_GET_EXTERN(r12, __objc_forward_handler)ldr	r12, [r12]bx	r12END_ENTRY __objc_msgForwardENTRY __objc_msgForward_stret

    _objc_msgForward_impcache

    中,调用

    __objc_msgForward

    ,然后调用

    __objc_forward_handler

    ,转掉

    _objc_forward_handler OC

    方法如下,然后就是经典崩溃。

    那么,在崩溃时,为什么会打印如上图的一系列堆栈信息呢 ?

    通过查看

    lookUpImpOrForward

    源码,如上图,当查找到

    IMP

    时,会调用

    log_and_fill_cache

    方法,进行缓存填充和日志存储。

    log_and_fill_cache

    如上图,通过控制

    objcMsgLogEnabled

    来控制日志存储,日志会记录在

    /tmp/msgSends

    目录下,而

    objcMsgLogEnabled

    的赋值是在

    instrumentObjcMessageSends

    之中,可以暴露这个方法,来达到外部打日志的操作。

    在查看

    /tmp/msgSends

    目录下的文件,如图:

    发现调用

    resolveInstanceMethod:

    ,

    forwardingTargetForSelector

    methodSignatureForSelector

    doesNotRecognizeSelector

    一系列方法,进行消息转发。

    2.1 快速转发流程

    通过查看

    forwardingTargetForSelector

    的官方文档,

    - (id)forwardingTargetForSelector:(SEL)aSelector

    方法,

    1. 返回一个对象。如果这个对象非空、非nil,系统会将消息转发给这个对象执行,否则,继续查找其他流程。系统给了将这个SEL转给其他对象的机会。2. 如果返回nil,或者没有处理消息转发,会走到forwardInvocation:方法进行处理,进入慢速消息转发流程。

    可以通过一下代码,将

    saySomething方法

    的消息转发到

    LGTeacher

    类中实现,而不会引起系统崩溃,至此消息快速转发结束。

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

    2.2 慢速转发流程

    进入慢速查找流程,首先必须先实现

    methodSignatureForSelector

    方法,返回一个签名,这个方法签名里面封装了返回值类型,参数类型等信息。

    - (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

    ;

    - (void)forwardInvocation:(NSInvocation *)invocation{SEL aSelector = [invocation selector];if ([friend respondsToSelector:aSelector])[invocation invokeWithTarget:friend];else[super forwardInvocation:invocation];}

    注意:

    1. forwardInvocation 方法和 methodSignatureForSelector 方法必须同时实现2. methodSignatureForSelector 会生成一个签名,NSInvocation对象,将NSInvocation对象作为参数传给 forwardInvocation 方法的3. 在forwardInvocation方法里面将消息给能处理该消息的对象,以避免对象调用didNotRecognizeSelector 方法导致崩溃4. forwardInvocation 这个方法类似于将消息当做事务堆放起来,在这里谁可以操作就在这里面操作,就算不操作也不会崩溃,这里也是防崩溃的最后处理机会。

    接下来看一下系统

    NSObject

    forwardInvocation

    的实现:

    + (void)forwardInvocation:(NSInvocation *)invocation {[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];}- (void)forwardInvocation:(NSInvocation *)invocation {[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];}// 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);}

    由此可见,系统最后是在

    doesNotRecognizeSelector

    方法中抛出异常的,所以重写

    forwardInvocation

    方法后,不管里面有么有实现,或者执行父类的方法,程序都是不会崩溃的。

    消息转发流程图:

    总结

    当调用了未实现的方法,三个解决途径:

    1、resolveInstanceMethod:为发送消息的对象的添加一个IMP,然后再让该对象去处理2、forwardingTargetForSelector:将该消息转发给能处理该消息的对象3、methodSignatureForSelector和 forwardInvocation:第一个方法生成方法签名,然后创建NSInvocation 对象作为参数给第二个方法,然后在forwardInvocation  方法里面做消息处理,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃

    关于

    resolveInstanceMethod

    方法调用两次的问题?

    1. 当查找
      IMP

      时,没有找到时,进入方法动态解析时,会第一次调用

      resolveInstanceMethod
    2. 然后进入消息转发流程,调用
      forwardingTargetForSelector

      ,将该消息转发给能处理该消息的对象

    3. 调用
      methodSignatureForSelector

      forwardInvocation

      ,返回签名

    4. 调用
      forwardInvocation


    在断点调试时,通过汇编,发现第二次

    resolveInstanceMethod

    调用在第三步和第四步之间,猜测,当签名处理完毕时,会匹配返回的签名和原始调用方法的签名,那么怎么找到原始调用方法的签名呢?重新发送一次消息。调用

    class_getInstanceMethod

    ,重新查找一次方法,再一次发送

    resolveInstanceMethod

    • 点赞1
    • 收藏
    • 分享
    • 文章举报

    亮亮不想说话发布了18 篇原创文章 · 获赞 10 · 访问量 264私信关注

    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » iOS 方法动态决议和消息转发机制