它会在运行时通过目标对象去寻找对应的方法,

在消息转发中提到过NSInvocation这个类,这里说一下我所理解的NSInvocationNSInvocation命令模式的一种实现,它包含选择器、方法签名、相应的参数以及目标对象。所谓的方法签名,即方法所对应的返回值类型和参数类型。当NSInvocation被调用,它会在运行时通过目标对象去寻找对应的方法,从而确保唯一性,可以用[receiver message]来解释。实际开发过程中直接创建NSInvocation的情况不多见,这些事情通常交给系统来做。比如bangJSPatcharm64方法替换的实现就是利用runtime消息转发最后一步中的NSInvocation实现的。

NSInvocation是命令模式的一种实现,它包含选择器、方法签名、相应的参数以及目标对象。所谓的方法签名,即方法所对应的返回值类型和参数类型。当NSInvocatio被调用,它会在运行时通过目标对象去寻找对应的方法,从而确保唯一性,可以用[receiver message]来解释。实际开发过程中直接创建NSInvocation的情况不多见,这些事情通常交给系统来做。比如bang的JSPatch中arm64方法替换的实现就是利用runtime消息转发最后一步中的NSInvocation实现的。

前言:

众所周知,使用runtime的提供的接口,我们可以设定原方法的IMP,或交换原方法和目标方法的IMP,以完全代替原方法的实现,或为原实现前后相当于加一段额外的代码。

先看几个概念:


代理:每个业务类都存在核心代码(当且业务独有的逻辑)和非核心代码(每个业务都可能有的通用逻辑)。目标类只完成核心业务,将非核心代码交给代理类完成具体实现。使核心逻辑更加简介、清晰。目标类和代理类要在编译期确定绑定关系。
动态代理:代理类完成通用非核心业务,在运行期动态绑定目标类。不需要修改核心代码,避免一个类对应一个代理,统一代理类对目标类的处理过程,减少重复代码。例如给一些类加统一的权限控制;日志系统;监控系统等都可以用动态代理实现。
AOP(Aspect Oriented Programming):面向切面编程,在不修改核心代码的情况下运行期给原程序添加额外的功能,使核心业务逻辑的附加功能完全隔离开,降低模块耦合度。动态AOP一般用动态代理的思想实现。如性能监测;访问控制;事务管理以及日志记录;线上热修复工具等,都参考了AOP设计。


isa:运行时指向当前对象的类对象。
Class:一个类也是一个对象,描述了一个类的所有实例方法、协议和属性。
meta-class:类对象的类,描述了一个类的所有类方法。class.png
method:一个包含了方法名、参数和返回值类型、imp的结构体。
imp:方法的真正实现(函数指针)。
type encoding:用字符串描述OC所有的数据类型。
selector:OC方法名(包括参数名和参数个数)。
NSMethodSignature:方法签名,记录了一个方法的返回值类型类型和参数。用于创建NSInvocation 。
NSInvocation:OC方法的静态描述,Invocation对象包含了OC方法所有的参数和返回值,用于消息转发(Invocation可以分发到其他target,所有参数都可以修,selector也可以转成另外一个具有相同方法签名的selector)。
方法调用:其实是对objc_msgSend(id self, SEL op, ...)的函数调用,根据self和SEL,先在当前class的方法缓存cache中匹配SEL,再从class的methodLists中匹配,没有找到再去父类中找,直到NSObject,如果都没有找到imp,则走消息转发流程。oc_message.png
消息转发:动态方法解析(动态添加一个方法来响应当前的消息)、快速消息转发(将该条消息发送给其他接收者来处理)、标准消息转发(不仅可以更改消息接受者,还可以通过invocaton更该方法参数和返回值,甚至修改SEL)、抛出异常。msg_forawrd.png

OC中所有具有isa和super_class指针的结构体都是一个对象,类本身也是一个对象,可以在运行时动态创建类对象。我们在运行时能够动态更改一个实例的行为,给他添加新方法或者修改原有方法,甚至修改类的指向,其实都是修改了类所指向的对象,或者修改了类的实例的isa指针,使他指向了其它的类对象。

Aspects正是利用了OC的这个特性,给我们提供了两个接口

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

第一个类方法:可以动态修改原有类中定义好的方法实,当被修改的类生成实例之后,就具有新的行为,实现了AOP设计。

图片 1

aop1.png

第二个实例方法:可以在类生成实例之后继承之前的类动态创建一个子类,并且修改子类中的方法,创建子类对象,再将原来类实例的isa指向子类对象,通过block将原类的invocation和实参回调给用户。可以实现动态修改一个对象的行为。
以上两个接口的区别是:类方法直接修改了原来的类,该类生成的所有实例都将被修改。第二个实例方法只修改了这个类当前的实例,不会对类其它实例造成影响。

图片 2

aop2.png

基于这种命令模式,可以利用NSInvocation调用任意SEL甚至block

正文

基于这种命令模式,可以利用NSInvocation调用任意SEL甚至block。NSInvocation与NSMethodSignature配合来完成调用。NSMethodSignature这个类的对象保存了方法的名称、参数和返回值

  • SEL
    系统NSObject自带的performSelector默认只支持一个参数,我们可以给NSObject增加一个category,增加以下方法就可以支持多个参数的调用了
- (id)performSelector:(SEL)aSelector withArguments:(NSArray *)arguments {
    if (aSelector == nil) return nil;
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = aSelector; // invocation 有2个隐藏参数,所以 argument 从2开始
    if ([arguments isKindOfClass:[NSArray class]]) {
        NSInteger count = MIN(arguments.count, signature.numberOfArguments - 2);
        for (int i = 0; i < count; i++) {
            const char *type = [signature getArgumentTypeAtIndex:2 + i]; // 需要做参数类型判断然后解析成对应类型,这里默认所有参数均为OC对象
            if (strcmp(type, "@") == 0) {
                id argument = arguments[i]; [invocation setArgument:&argument atIndex:2 + i];
            }
        }
    }
    [invocation invoke];
    id returnVal;
    if (strcmp(signature.methodReturnType, "@") == 0) {
        [invocation getReturnValue:&returnVal];
    } // 需要做返回类型判断。比如返回值为常量需要包装成对象,这里仅以最简单的`@`为例
    return returnVal;
}

图片 3

ED3CFE7D-C30A-4ADD-929D-B5F7CB806E82.png

图片 4

FCB4F1F5-1F15-4FDC-A3AE-C1DC95AF32BF.png

unrecognized selector类型的crash是因为一个对象调用了一个不属于它的方法导致的。要解决这种类型的crash,我们先要了解清楚它产生的具体原因和流程。本文先讲了消息传递机制和消息转发机制的流程,然后对消息转发流程的一些函数的使用进行举例,最后指出了对“unrecognized selector类型的crash”的防护措施。

@interface ClassA: NSObject- methodA;+ methodB;@end...@implementation ClassA + load { Method originalMethod = class_getInstanceMethod(self, @selector; Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_methodA)); method_exchangeImplementations(originalMethod, swizzledMethod);}- swizzled_methodA { ... [self swizzled_methodA]; ...}@end

Aspects实现流程


1、 Check against the blacklist

  1) 下面这些方法不会hook。 
     @"retain"
     @"release",
     @"autorelease"
     @"forwardInvocation:"

  2) 对@"dealloc"进行hook只能使用  
     AspectPositionBefore

  3) 不存在的方法不会hook。
     如果是meta class还会执行4) 5)
     如果hook的是instance执行跳到第2步

  4) 便利当前类和父类直到NSObject
     定位到需要hook的方法所在的类,在
     缓存(swizzledClassesDict)中查该
     方法是否已经被hook。一个类中的一
     个方法,只能被hook一次。

  5) 再次便利当前类和父类
     将需要hook的方法做标记
     存在缓存(swizzledClassesDict)中。   

  ps:4、5两步是否存在性能问题?

2、 生成AspectsContainer

  1) 给原方法起别名(加aspects__前缀)
     生成AspectsContainer实例。

  2) 以别名为key,将生成AspectsContainer
     作为一个属性添加给当前类。

3、 生成blockSignature

  1) 解析调用者传进来的block
    (block的结构比较复杂,要另外讨论)

  2) 如果调用者没有传入id则不需要生成方法签名

  3) 如果传入了id,则生成方block法签名。

  4) 根据原方法的@selector生成方法签名signature
     并且和blockSignature做匹配

    a.对比参数个数

    b.只校验原方法的signature和blockSignature
      参数类型和个数是否一致。

    c.前2个参数不一样,分别是:
      argument 0(@ 代表self或者block)     
      argument 1 (: 代表selector)。

  5) 校验结束,根据原方法
     selector,block,blockSignature,
     options,self(类或者实例,弱引用)
     生成AspectIdentifier可用于取消hook。

  6) 将生成好的AspectIndetifer对象
     加入第2步生成的AspectsContainer对象中

4、 修改原类,对其hook的方法进行消息转发拦截

  1) 准备hook原类(如果是调用的是类方法则做消息转发拦截,如果是实例方法会动态生成子类)

    a.检测原class是否已经存在_Aspects_前缀
      有的话直接返回原来的class

    b.判断是hook原类还是hook类的实例

      1. hook meta_class

         a) 将原类forwardInvocation方法IPM
            替换成__ASPECTS_ARE_BEING_CALLED__

         b) 如果class_replaceMethod返回原IMP
            在添加一个__aspects_forwardInvocation方法
            方法IMP就是原来forwardInvocation的IMP
            作用是调用unregister的时候在把原来的forwardInvocation方法替换回来
            但这里测试下来发现class_replaceMethod没有返回原IMP?

         c) !!!运行时(其它都是非运行时对class的操作): 

              1)__aspects_forwardInvocation用原方法的invocation
               将invocation的实参替换blockInvocation的参数 

              2).将原方法Invocation、当前self、方法参数列表
                 包装成id<AspectInfo>对象
                 塞给blockInvocation的第一个参数
                 调用者可以通过AspectInfo在block中取到原方法的Invocation和参数

              3)方法执行,三种模式,调用顺序:
                AspectPositionBefore:执行block、执行原方法invocation。    
                AspectPositionAfter: 执行原方法invocation,执行block。     
                AspectPositionInstead:执行block、要手动执行原方法的invocation。

              4) 执行过程中如果aspectsContainer中找不到需要替换的方法
                 调__aspects_forwardInvocation(IMP:原forwardInvocation)走消息转发
                 forwardInvocation无法响应则掉用doesNotRecognizeSelector
                 !!!b)流程class_replaceMethod的一个bug,它没有返回
                 原forwardInvocation方法的IMP导致__aspects_forwardInvocation的
                 IMP为空,因此一定会走doesNotRecognizeSelector。
                 ps:如果使用了像《JSPatch》这种也将hook的方法换行成forwardInvocation
                 走到这里就会Crash。

       2. hook instance

         a) 根据原类,创建一个名为(originalClassName__Aspects_)的新class和metaclass,并创建类对象

         b) 将新类forwardInvocation方法IPM替换成__ASPECTS_ARE_BEING_CALLED__。

         c) 替换新类的class方法,新IMP是一个"Class (^block)(id self)";
            这个block返回原类的Class;
            替换后新类的实例调用getClass方法时返回的是原类的Class;
            因为self虽然赋值给了新生成的Class;
            但是self的MethodList中记录的Class,SEL,IMP对应关系没变;
            要通过class_getInstanceMethod获取SEL的Method;
            或者instanceMethodSignatureForSelector获取方法MethodSignature
            还需要用原Class。

         d) 替换新类的class方法,同:c)。

         e) 注册新class;
            调用object_setClass方法将当前self实例赋值给成新生成的类。;
            此时新的class的实例已经变成self;
            self的内存地址没变只是类的结构变成了新生成的Class。

         f) !!!运行时(其它都是非运行时对class的操作):
            同meta_class方法

  2) 在全局集合swizzledClasses中加入原类名,做缓存使用。并且返回上一步生成的class。

  3) 根据上一步返回的class,获取原方法的IMP。

  4) 判断上一步的IMP是否是_objc_msgForward
     如果是则,调用原方法就是直接会走消息转发流程,如果不是会进行下一步

  5) 给class新增一个aspects_selector指向原IMP(unregister时候使用)。

  6) 将原IMP替换成_objc_msgForward(作用是:调用原方法直接进行消息转发)。
SEL
- performSelector:aSelector withArguments:(NSArray *)arguments { if (aSelector == nil) return nil; NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = self; invocation.selector = aSelector; // invocation 有2个隐藏参数,所以 argument 从2开始 if ([arguments isKindOfClass:[NSArray class]]) { NSInteger count = MIN(arguments.count, signature.numberOfArguments - 2); for (int i = 0; i < count; i++) { const char *type = [signature getArgumentTypeAtIndex:2 + i]; // 需要做参数类型判断然后解析成对应类型,这里默认所有参数均为OC对象 if (strcmp(type, "@") == 0) { id argument = arguments[i]; [invocation setArgument:&argument atIndex:2 + i]; } } } [invocation invoke]; id returnVal; if (strcmp(signature.methodReturnType, "@") == 0) { [invocation getReturnValue:&returnVal]; } // 需要做返回类型判断。比如返回值为常量需要包装成对象,这里仅以最简单的`@`为例 return returnVal;}

图片 5运行结果

NSObject中的performSelector相比,没有了参数个数限制。

图片 6invocation图片 7signature

block

static id invokeBlock(id block ,NSArray *arguments) {
    if (block == nil) return nil;
    id target = [block  copy];

    const char *_Block_signature(void *);
    const char *signature = _Block_signature((__bridge void *)target);

    NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:signature];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.target = target;

    // invocation 有1个隐藏参数,所以 argument 从1开始
    if ([arguments isKindOfClass:[NSArray class]]) {
        NSInteger count = MIN(arguments.count, methodSignature.numberOfArguments - 1);
        for (int i = 0; i < count; i++) {
            const char *type = [methodSignature getArgumentTypeAtIndex:1 + i];
            NSString *typeStr = [NSString stringWithUTF8String:type];
            if ([typeStr containsString:@"""]) {
                type = [typeStr substringToIndex:1].UTF8String;
            }

            // 需要做参数类型判断然后解析成对应类型,这里默认所有参数均为OC对象
            if (strcmp(type, "@") == 0) {
                id argument = arguments[i];
                [invocation setArgument:&argument atIndex:1 + i];
            }
        }
    }

    [invocation invoke];

    id returnVal;
    const char *type = methodSignature.methodReturnType;
    NSString *returnType = [NSString stringWithUTF8String:type];
    if ([returnType containsString:@"""]) {
        type = [returnType substringToIndex:1].UTF8String;
    }
    if (strcmp(type, "@") == 0) {
        [invocation getReturnValue:&returnVal];
    }
    // 需要做返回类型判断。比如返回值为常量需要包装成对象,这里仅以最简单的`@`为例
    return returnVal;
}

图片 8

9E8A9D90-F9A9-4DD3-AA21-9953E4953C8D.png

图片 9

一、消息传递机制和消息转发机制

使用知名的AOP库 Aspects ,可以更便捷地为原方法实现前后增加额外的执行。

block
static id invokeBlock(id block ,NSArray *arguments) { if (block == nil) return nil; id target = [block copy]; const char *_Block_signature; const char *signature = _Block_signature((__bridge void *)target); NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:signature]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocation.target = target; // invocation 有1个隐藏参数,所以 argument 从1开始 if ([arguments isKindOfClass:[NSArray class]]) { NSInteger count = MIN(arguments.count, methodSignature.numberOfArguments - 1); for (int i = 0; i < count; i++) { const char *type = [methodSignature getArgumentTypeAtIndex:1 + i]; NSString *typeStr = [NSString stringWithUTF8String:type]; if ([typeStr containsString:@"""]) { type = [typeStr substringToIndex:1].UTF8String; } // 需要做参数类型判断然后解析成对应类型,这里默认所有参数均为OC对象 if (strcmp(type, "@") == 0) { id argument = arguments[i]; [invocation setArgument:&argument atIndex:1 + i]; } } } [invocation invoke]; id returnVal; const char *type = methodSignature.methodReturnType; NSString *returnType = [NSString stringWithUTF8String:type]; if ([returnType containsString:@"""]) { type = [returnType substringToIndex:1].UTF8String; } if (strcmp(type, "@") == 0) { [invocation getReturnValue:&returnVal]; } // 需要做返回类型判断。比如返回值为常量需要包装成对象,这里仅以最简单的`@`为例 return returnVal;}

图片 10运行结果图片 11invocation图片 12signature

SEL与block比较

  • invocation
    SEL既有target也有selector,block只有target
  • signature
    SEL有两个隐藏参数,类型均为@,分别对应target和selector。block有一个隐藏参数,类型为@?,对应target且block的target为他本身
  • type
    以OC对象为例:SEL的type为@,block的type会跟上具体类型,如@"NSString"

1.  消息传递机制(动态消息派发系统的工作过程)

// hook instance method[ClassA aspect_hookSelector:@selector withOptions:AspectPositionAfter usingBlock:^{...} error:nil]; // hook class method[object_getClass aspect_hookSelector:@selector withOptions:AspectPositionAfter usingBlock:^{...} error:nil];
SEL与block比较
  • invocationSEL既有target也有selector,block只有target
  • signatureSEL有两个隐藏参数,类型均为`@` 类型为@: ,分别对应target和selector。block有一个隐藏参数,类型为@?,对应target且block的target为他本身
  • type以OC对象为例:SEL的type为@,block的type会跟上具体类型,如@"NSString"

再谈block

在block的invocation中有这样的代码

const char *_Block_signature(void *);
const char *signature = _Block_signature((__bridge void *)target);

_Block_signature其实是JavaScriptCore/ObjcRuntimeExtras.h
中的私有API(这个头文件并没有公开可以戳这里查看)

既然苹果把API封了,那就自己实现咯,万能的github早有答案CTObjectiveCRuntimeAdditions把CTBlockDescription.h
和CTBlockDescription.m
拖到项目中,代码这样写

static id invokeBlock(id block ,NSArray *arguments) {
    if (block == nil) return nil;
    id target = [block  copy];

    CTBlockDescription *ct = [[CTBlockDescription alloc] initWithBlock:target];
    NSMethodSignature *methodSignature = ct.blockSignature;
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.target = target;

    // invocation 有1个隐藏参数,所以 argument 从1开始
    if ([arguments isKindOfClass:[NSArray class]]) {
        NSInteger count = MIN(arguments.count, methodSignature.numberOfArguments - 1);
        for (int i = 0; i < count; i++) {
            const char *type = [methodSignature getArgumentTypeAtIndex:1 + i];
            NSString *typeStr = [NSString stringWithUTF8String:type];
            if ([typeStr containsString:@"""]) {
                type = [typeStr substringToIndex:1].UTF8String;
            }

            // 需要做参数类型判断然后解析成对应类型,这里默认所有参数均为OC对象
            if (strcmp(type, "@") == 0) {
                id argument = arguments[i];
                [invocation setArgument:&argument atIndex:1 + i];
            }
        }
    }

    [invocation invoke];

    id returnVal;
    const char *type = methodSignature.methodReturnType;
    NSString *returnType = [NSString stringWithUTF8String:type];
    if ([returnType containsString:@"""]) {
        type = [returnType substringToIndex:1].UTF8String;
    }
    if (strcmp(type, "@") == 0) {
        [invocation getReturnValue:&returnVal];
    }
    // 需要做返回类型判断。比如返回值为常量需要包装成对象,这里仅以最简单的`@`为例
    return returnVal;
}

图片 13

17D15BD3-98AA-46E9-BAD4-2747E16DD677.png

当编译器收到[someObject messageName:parameter]消息后,编译器会将此消息转换为调用标准的C语言函数objc_msgSend,如下所示:

另外,Aspects 支持多次hook同一个方法,支持从hook返回的id<AspectToken>对象删除对应的hook。</br> IMP即函数指针,Aspects 的大致原理:替换原方法的IMP消息转发函数指针 _objc_msgForward_objc_msgForward_stret,把原方法IMP添加并对应到SEL aspects_originalSelector,将forwardInvocation:IMP替换为参数对齐的C函数__ASPECTS_ARE_BEING_CALLED__(NSObject *self, SEL selector, NSInvocation *invocation)的指针。在__ASPECTS_ARE_BEING_CALLED__函数中,替换invocationselectoraspects_originalSelector,相当于要发送调用原始方法实现的消息。对于插入位置在前面,替换,后面的多个block,构建新的blockInvocation,从invocation中提取参数,最后通过invokeWithTarget:block来完成依次调用。有关消息转发的介绍,可以参考笔者的另一篇文章用代码理解ObjC中的发送消息和消息转发。</br> Aspects 实现代码里的很多细节处理是很令人称道的,且支持hook类的单个实例对象的方法(类似于KVO的isa-swizzlling)。但由于对原方法调用直接进行了消息转发,到真正的IMP对应的函数被执行前,经历了对其他多个消息的处理,invoke block也需要额外的invocation构建开销。作者也在注释中写道,不适合对每秒钟超过1000次的方法增加切面代码。此外,使用其他方式对Aspect hook过的方法进行hook时,如直接替换为新的IMP,新hook得到的原始实现是_objc_msgForward,之前的aspect_hook会失效,新的hook也将执行异常。</br> 那么不禁要思考,有没有一种方式可以替换原方法的IMP为一个和原方法参数相同(type encoding)的方法的函数指针,作为壳,处理消息时,在这个壳内部拿到所有参数,最后通过函数指针直接执行“前”、“原始/替换”,“后”的多个代码块。令人惊喜的是,libffi 可以帮我们做到这一切。

再谈block

在block的invocation中有这样的代码

 const char *_Block_signature; const char *signature = _Block_signature((__bridge void *)target);

_Block_signature其实是JavaScriptCore/ObjcRuntimeExtras.h中的私有API(这个头文件并没有公开可以戳这里查看)

既然苹果把API封了,那就自己实现咯,万能的github早有答案CTObjectiveCRuntimeAdditions把CTBlockDescription.hCTBlockDescription.m拖到项目中,代码这样写

static id invokeBlock(id block ,NSArray *arguments) { if (block == nil) return nil; id target = [block copy]; CTBlockDescription *ct = [[CTBlockDescription alloc] initWithBlock:target]; NSMethodSignature *methodSignature = ct.blockSignature; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocation.target = target; // invocation 有1个隐藏参数,所以 argument 从1开始 if ([arguments isKindOfClass:[NSArray class]]) { NSInteger count = MIN(arguments.count, methodSignature.numberOfArguments - 1); for (int i = 0; i < count; i++) { const char *type = [methodSignature getArgumentTypeAtIndex:1 + i]; NSString *typeStr = [NSString stringWithUTF8String:type]; if ([typeStr containsString:@"""]) { type = [typeStr substringToIndex:1].UTF8String; } // 需要做参数类型判断然后解析成对应类型,这里默认所有参数均为OC对象 if (strcmp(type, "@") == 0) { id argument = arguments[i]; [invocation setArgument:&argument atIndex:1 + i]; } } } [invocation invoke]; id returnVal; const char *type = methodSignature.methodReturnType; NSString *returnType = [NSString stringWithUTF8String:type]; if ([returnType containsString:@"""]) { type = [returnType substringToIndex:1].UTF8String; } if (strcmp(type, "@") == 0) { [invocation getReturnValue:&returnVal]; } // 需要做返回类型判断。比如返回值为常量需要包装成对象,这里仅以最简单的`@`为例 return returnVal;}

图片 14运行结果

NSInvocation.h

@interface NSInvocation : NSObject {
@private
    __strong void *_frame;
    __strong void *_retdata;
    id _signature;
    id _container;
    uint8_t _retainedArgs;
    uint8_t _reserved[15];
}

// 通过NSMethodSignature对象创建NSInvocation对象,NSMethodSignature为方法签名类
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

// 获取NSMethodSignature对象
@property (readonly, retain) NSMethodSignature *methodSignature;

// 保留参数,它会将传入的所有参数以及target都retain一遍
- (void)retainArguments;

// 判断参数是否还存在
// 调用retainArguments之前,值为NO,调用之后值为YES
@property (readonly) BOOL argumentsRetained;

// 设置消息调用者,注意:target最好不要是局部变量
@property (nullable, assign) id target;

// 设置要调用的消息
@property SEL selector;

// 获取消息返回值
- (void)getReturnValue:(void *)retLoc;

// 设置消息返回值
- (void)setReturnValue:(void *)retLoc;

// 获取消息参数
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

// 设置消息参数
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

// 发送消息,即执行方法
- (void)invoke;

// target发送消息,既设置了target同时执行方法。这一步要注意在调用此方法之前必须设置了selector和argument
- (void)invokeWithTarget:(id)target;

@end

objc_msgSend(someObject,@selector(messageName:),parameter)

libffi 可以认为是实现了C语言上的runtime,简单来说,libffi 可根据 参数类型(ffi_type),参数个数 生成一个 模板(ffi_cif);可以输入 模板函数指针参数地址 来直接完成 函数调用(ffi_call); 模板 也可以生成一个所谓的 闭包(ffi_closure),并得到指针,当执行到这个地址时,会执行到自定义的void function(ffi_cif *cif, void *ret, void **args, void *userdata)函数,在这里,我们可以获得所有参数的地址,以及自定义数据userdata。当然,在这个函数里我们可以做一些额外的操作。</br>

 该方法会去someObject所属的类中搜寻其“方法列表”,如果能找到与messageName:相符的方法,就跳转到实现代码;找不到就沿着继承体系继续向上找;如果最终还是找不到,就执行“消息转发”操作。

根据参数个数和参数类型生成的各自的ffi_type。

2. 消息转发机制

int fun1 (int a, int b) { return a + b;}int fun2 (int a, int b) { return 2 * a + b;}...ffi_type **types; // 参数类型types = malloc(sizeof(ffi_type *) * 2) ;types[0] = &ffi_type_sint;types[1] = &ffi_type_sint;ffi_type *retType = &ffi_type_sint;

消息转发分两大阶段:

根据ffi_type生成特定cif,输入cif、 函数指针、参数地址动态调用函数。

(1)动态方法解析:即征询selector所属的类的下列方法,看其是否能动态添加这个未知的选择子:

void **args = malloc(sizeof * 2);int x = 1, y = 2;args[0] = &x;args[1] = &y;int ret;ffi_cif cif; // 生成模板ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, types); // 动态调用fun1ffi_call(&cif, fun1, &ret, args);...// 输出: ret = 3;

//  缺失的selector是实例方法调用+(BOOL)resolveInstanceMethod:(SEL)selector

生成closure,并产生一个函数指针imp,当执行到imp时,获得所有输入参数, 后续将执行ffi_function。

//  缺失的selector是类方法调用+(BOOL)resolveClassMethod:(SEL)selector

void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) { ... // args为所有参数的内存地址}ffi_cif cif; // 生成模板ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, returnType, types);ffi_prep_closure_loc(_closure, &_cif, ffi_function, (__bridge void *), imp);void *imp = NULL;ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &imp);//生成ffi_closureffi_prep_closure_loc(closure, &cif, ffi_function, (__bridge void *), stingerIMP);

该方法的参数就是那个未知的选择子,其返回值Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。(@dynamic属性没有实现setter方法和getter方法,可以在“消息转发”过程对其实现)

libffi 能调用任意 C 函数的原理与objc_msgSend的原理类似,其底层是用汇编实现的,ffi_call根据模板cif和参数值,把参数都按规则塞到栈/寄存器里,调用的函数可以按规则得到参数,调用完再获取返回值,清理数据。通过其他方式调用上文中的imp,ffi_closure可根据栈/寄存器、模板cif拿到所有的参数,接着执行自定义函数ffi_function里的代码。JPBlock的实现正是利用了后一种方式,更多细节介绍可以参考 bang: 如何动态调用 C 函数。</br> 到这里,对于如何hook ObjC方法和实现AOP,想必大家已经有了一些思路,我们可以将ffi_closure关联的指针替换原方法的IMP,当对象收到该方法的消息时objc_msgSend(id self, SEL sel, ...),将最终执行自定义函数void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)。而实现这一切的主要工作是:设计可行的结构,存储类的多个hook信息;根据包含不同参数的方法和切面block,生成包含匹配ffi_type的cif;替换类某个方法的实现为ffi_closure关联的imp,记录hook;在ffi_function里,根据获得的参数,动态调用原始imp和block。</br>

(2)消息转发

#import <Foundation/Foundation.h>#import "StingerParams.h"typedef NSString *STIdentifier;typedef NS_ENUM(NSInteger, STOption) { STOptionAfter = 0, STOptionInstead = 1, STOptionBefore = 2,};@interface NSObject + st_hookInstanceMethod:sel option:option usingIdentifier:(STIdentifier)identifier withBlock:block;+ st_hookClassMethod:sel option:option usingIdentifier:(STIdentifier)identifier withBlock:block;+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:key;+ st_removeHookWithIdentifier:(STIdentifier)identifier forKey:key;@end

(2.1)“备援接收者”方案----当前接收者第二次处理未知选择子的机会:运行期系统通过下列方法问当前接收者,能不能把这条消息转发给其它接收者来处理:

下文将围绕一些重要的点来介绍下笔者的实现。Stinger

-(id)forwardingTargetForSelector:(SEL)selector

2.1 方法签名 -> ffi_type

对于方法的签名和type encoding,笔者在 用代码理解ObjC中的发送消息和消息转发 一文中已经有了不少介绍。简而言之,type encoding 字符串与方法的返回类型及参数类型是一一对应的。例如:- print1:(NSString *)s;的type encoding为v24@0:8@16v对应void@对应id:对应SEL@对应id(这里是NSString *),另一方面,每一种参数类型都对应一种ffi_type,如v对应ffi_type_void, @对应ffi_type_pointer。可以用type encoding生成一个NSMethodSignature实例对象,利用numberOfArguments- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx;方法获取每一个位置上的参数类型。当然,也可以过滤掉数字来分隔字符串v24@0:8@16,得到参数类型数组(JSPatch中使用了这一方式)。接着,我们对字符和ffi_type做一一对应即可完成从方法签名到ffi_type的转换。

_args = malloc(sizeof(ffi_type *) * argumentCount) ;for (int i = 0; i < argumentCount; i++) { ffi_type* current_ffi_type = ffiTypeWithType(self.signature.argumentTypes[i]); NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]); _args[i] = current_ffi_type;}

该方法的参数就是那个未知的选择子,其返回值id类型,表示找到的备援对象,找不到就返回nil。(缺点:我们无法操作经由这一步所转发的消息。)

2.2 浅谈block

void (id<StingerParams> params, NSString *s) = ^(id<StingerParams> params, NSString *s) { NSLog(@"---after2 print1: %@", s);}

block是一个ObjC对象,可以认为几种block类型都继承于NSBlock。block很特殊,从表面来看包含了持有了数据和对象,并拥有可执行的代码,调用方式类似于调用C函数,等同于数据加函数。Block类型很神秘,但我们从 opensource-apple/objc4 和 oclang/docs/block 中看到Block 完整的数据结构。

enum { BLOCK_DEALLOCATING = , // runtime BLOCK_REFCOUNT_MASK = , // runtime BLOCK_NEEDS_FREE = (1 << 24), // runtime BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code BLOCK_IS_GC = (1 << 27), // runtime BLOCK_IS_GLOBAL = (1 << 28), // compiler BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30) // compiler};// revised new layout#define BLOCK_DESCRIPTOR_1 1struct Block_descriptor_1 { unsigned long int reserved; unsigned long int size;};#define BLOCK_DESCRIPTOR_2 1struct Block_descriptor_2 { // requires BLOCK_HAS_COPY_DISPOSE void (void *dst, const void *src); void (const void *);};#define BLOCK_DESCRIPTOR_3 1struct Block_descriptor_3 { // requires BLOCK_HAS_SIGNATURE const char *signature; const char *layout;};struct Block_layout { void *isa; volatile int flags; // contains ref count int reserved; void (void *, ...); struct Block_descriptor_1 *descriptor; // imported variables};

很多人大概已经看过BlocksKit的代码,了解到Block对象可以强转为Block_layout类型,通过标识符和内存地址偏移获取block的签名signature

NSString *signatureForBlock { struct Block_layout *layout = (__bridge void *)block; if (!(layout->flags & BLOCK_HAS_SIGNATURE)) return nil; void *descRef = layout->descriptor; descRef += 2 * sizeof(unsigned long int); if (layout->flags & BLOCK_HAS_COPY_DISPOSE) descRef += 2 * sizeof; if  return nil; const char *signature = (*(const char **)descRef); return [NSString stringWithUTF8String:signature];}

NSString *signature = signatureForBlock// 输出 NSString:@"v24@?0@"<StingerParams>"8@"NSString"16"

对于Block对象的的最简签名,我们仍然可以构建NSMethodSignature来逐一获取,也可以通过过滤掉数字及'"'来获得字符数组。

_argumentTypes = [[NSMutableArray alloc] init];NSInteger descNum = 0; // num of '"' in block signature type encodingfor (int i = 0; i < _types.length; i ++) { unichar c = [_types characterAtIndex:i]; NSString *arg; if (c == '"') ++descNum; if ((descNum % 2) != 0 || (c == '"' || isdigit { continue; } ...} /*@"v24@?0@"<StingerParams>"8@"NSString"16"*/ -> v,@?,@,@

可以看到,签名的第一位是"@?",意味着第一个参数为blcok自己,后面的才是blcok的参数类型。同理,我们依然可以通过type encoding匹配到对应的ffi_type。</br> 此外,我们可以直接获取到Block对象的函数指针。

BlockIMP impForBlock { struct Block_layout *layout = (__bridge void *)block; return layout->invoke;}

做一个简单的尝试,直接调用Block对象的包含的函数。

void (NSString *s) = ^(NSString *s) { NSLog(@"---after2 print1: %@", s); }; void (*blockIMP) (id block, NSString *s) =  (id block, NSString *s))impForBlock; blockIMP(block2, @"tt"); // 输出:---after2 print1: tt

此外,实测通过IMP _Nonnull imp_implementationWithBlock(id _Nonnull block)获得的函数指针对应的参数并不包含Block对象自身,意味着签名发生了变化。

通过一些方式,我们可以觉得Block对象拥有了新的实例方法。

NSString *signature = [block signature];void *blockIMP = [block blockIMP];

做法是在STBlock里为NSBlock类增加实例方法。

typedef void *BlockIMP;@interface STBlock : NSObject+ (instancetype)new NS_UNAVAILABLE;- (instancetype)init NS_UNAVAILABLE;- (NSString *)signature;- blockIMP;NSString *signatureForBlock;BlockIMP impForBlock;@end

#define NSBlock NSClassFromString(@"NSBlock")void addInstanceMethodForBlock { Method m = class_getInstanceMethod(STBlock.class, sel); if  return; IMP imp = method_getImplementation; const char *typeEncoding = method_getTypeEncoding; class_addMethod(NSBlock, sel, imp, typeEncoding);}@implementation STBlock+ load { addInstanceMethodForBlock(@selector(signature)); addInstanceMethodForBlock(@selector);}...@end

这样做,可以为Block对象增加可处理的消息。但如果在其他类的load方法里尝试调用,可能会遇到STBlock类里load方法未加载的问题。

 (2.2) 完整的消息转发

3.1 StingerInfo

这里使用简单的对象来存储单个hook信息。

@protocol StingerInfo <NSObject>@required@property (nonatomic, copy) id block;@property (nonatomic, assign) STOption option;@property (nonatomic, copy) STIdentifier identifier;@optional+ (instancetype)infoWithOption:option withIdentifier:(STIdentifier)identifier withBlock:block;@end@interface StingerInfo : NSObject <StingerInfo>@end

调用下列方法转发消息:

3.2 StingerInfoPool

typedef void *StingerIMP;@protocol StingerInfoPool <NSObject>@required@property (nonatomic, strong, readonly) NSMutableArray<id<StingerInfo>> *beforeInfos;@property (nonatomic, strong, readonly) NSMutableArray<id<StingerInfo>> *insteadInfos;@property (nonatomic, strong, readonly) NSMutableArray<id<StingerInfo>> *afterInfos;@property (nonatomic, strong, readonly) NSMutableArray<NSString *> *identifiers;@property (nonatomic, copy) NSString *typeEncoding;@property (nonatomic) IMP originalIMP;@property (nonatomic) SEL sel;- (StingerIMP)stingerIMP;- addInfo:(id<StingerInfo>)info;- removeInfoForIdentifier:(STIdentifier)identifier;@optional@property (nonatomic, weak) Class cls;+ (instancetype)poolWithTypeEncoding:(NSString *)typeEncoding originalIMP:imp selector:sel;@end@interface StingerInfoPool : NSObject <StingerInfoPool>@end

这里利用三个数组来存储某个类hook位置在原实现前、替换、实现后的id<StingerInfo>对象,并保存了原始imp。添加和删除id<StingerInfo>对象的操作是线程安全的。

根据原始方法提供的type encoding,生成各个参数对应的ffi_type,继而生成cif对象,最后调用ffi_prep_closure_loc相当于生成空壳函数StingerIMP。调用StingerIMP将最终执行到自定义的static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)函数,此函数可获得调用StingerIMP时获得的所有参数。

- (StingerIMP)stingerIMP { ffi_type *returnType = ffiTypeWithType(self.signature.returnType); NSAssert(returnType, @"can't find a ffi_type of %@", self.signature.returnType); NSUInteger argumentCount = self.signature.argumentTypes.count; StingerIMP stingerIMP = NULL; _args = malloc(sizeof(ffi_type *) * argumentCount) ; for (int i = 0; i < argumentCount; i++) { ffi_type* current_ffi_type = ffiTypeWithType(self.signature.argumentTypes[i]); NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]); _args[i] = current_ffi_type; } _closure = ffi_closure_alloc(sizeof(ffi_closure), &stingerIMP); if(ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args) == FFI_OK) { if (ffi_prep_closure_loc(_closure, &_cif, ffi_function, (__bridge void *), stingerIMP) != FFI_OK) { NSAssert(NO, @"genarate IMP failed"); } } else { NSAssert(NO, @"FUCK"); } [self _genarateBlockCif]; return stingerIMP;}

与前面生成方法调用模板cif类似,只不过这里没有生成壳子ffi_closure。值得注意的是,这里把原始方法type encoing的第0位(@ self)和第1位(: SEL)替换为(@?block)和(@ id<StingerParams>)。意味着,限定了切面Block对象的签名类型。

- _genarateBlockCif { ffi_type *returnType = ffiTypeWithType(self.signature.returnType); NSUInteger argumentCount = self.signature.argumentTypes.count; _blockArgs = malloc(sizeof(ffi_type *) *argumentCount); ffi_type *current_ffi_type_0 = ffiTypeWithType; _blockArgs[0] = current_ffi_type_0; ffi_type *current_ffi_type_1 = ffiTypeWithType; _blockArgs[1] = current_ffi_type_1; for (int i = 2; i < argumentCount; i++){ ffi_type* current_ffi_type = ffiTypeWithType(self.signature.argumentTypes[i]); _blockArgs[i] = current_ffi_type; } if(ffi_prep_cif(&_blockCif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _blockArgs) != FFI_OK) { NSAssert(NO, @"FUCK"); }}

在非instead位置,block的返回值可以为任意;写block时,block的第0位(不考虑block自身)参数类型应该为id,后面接的是与原方法对应的参数。

在这个函数里,获取到了调用原始方法时的所有入参的内存地址,先是根据block_cif模板生成新的参数集innerArgs,第0位留给Block对象,第1位留给StingerParams对象,从第2位开始复制原始的参数。</br> 以下是完成切面代码和原始imp执行的过程:</br> 1. 利用ffi_call(&(self->_blockCif), impForBlock, NULL, innerArgs);完成所有切面位置在前block的调用。使用block模板blockCif和innerArgs。</br> 2. 利用ffi_call(cif, self.originalIMP / impForBlock, ret, args);完成对原始IMP或替换位置block imp的调用。使用原始模板cif和原始参数args,并可能产生返回值。</br> 3. 利用ffi_call(&(self->_blockCif), impForBlock, NULL, innerArgs);完成所有切面位置在后的block的调用。使用block模板blockCif和innerArgs。</br>

static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) { StingerInfoPool *self = (__bridge StingerInfoPool *)userdata; NSUInteger count = self.signature.argumentTypes.count; void **innerArgs = malloc(count * sizeof(*innerArgs)); StingerParams *params = [[StingerParams alloc] init]; void **slf = args[0]; params.slf = (__bridge id); params.sel = self.sel; [params addOriginalIMP:self.originalIMP]; NSInvocation *originalInvocation = [NSInvocation invocationWithMethodSignature:self.ns_signature]; for (int i = 0; i < count; i ++) { [originalInvocation setArgument:args[i] atIndex:i]; } [params addOriginalInvocation:originalInvocation]; innerArgs[1] = &params; memcpy(innerArgs + 2, args + 2, (count - 2) * sizeof; #define ffi_call_infos  for (id<StingerInfo> info in infos) {  id block = info.block;  innerArgs[0] = &block;  ffi_call(&(self->_blockCif), impForBlock, NULL, innerArgs);  }  // before hooks ffi_call_infos(self.beforeInfos); // instead hooks if (self.insteadInfos.count) { id <StingerInfo> info = self.insteadInfos[0]; id block = info.block; innerArgs[0] = &block; ffi_call(&(self->_blockCif), impForBlock, ret, innerArgs); } else { // original IMP ffi_call(cif, self.originalIMP, ret, args); } // after hooks ffi_call_infos(self.afterInfos); free(innerArgs);}

注:StingerParams 对象包含了消息接收者slf,当前消息的selector sel, 还包含了可调用原始方法的invocation(使用invokeUsingIMP:完成调用),该invocation仅适合在替换方法且需要原始返回值作参数时调用。其他hook直接使用optionBefore或after即可, 不用关注该invocation。</br>

#import <Foundation/Foundation.h>#define ST_NO_RET NULL@protocol StingerParams@required@property (nonatomic, unsafe_unretained) id slf;@property (nonatomic) SEL sel;- invokeAndGetOriginalRetValue:retLoc;@end@interface StingerParams : NSObject <StingerParams>- addOriginalInvocation:(NSInvocation *)invocation;- addOriginalIMP:imp;@end

思路是对某个类以SEL sel为键关联一个id<StingerInfoPool>对象,第一次hook,新建该对象,尝试替换原方法实现为ffi_prep_closure_loc关联的IMP,后续hook时,将直接添加hook info到关联的id<StingerInfoPool>对象中。</br> 关于条件,最主要的就是两点,第一点就是对于某个类中的某个SEL sel要能找到对应Method m及IMP imp;第二点即切面block与原方法的签名是匹配的,且切面block的签名是符合要求的(isMatched方法)。

#import "Stinger.h"#import <objc/runtime.h>#import "StingerInfo.h"#import "StingerInfoPool.h"#import "STBlock.h"#import "STMethodSignature.h"@implementation NSObject #pragma - public+ st_hookInstanceMethod:sel option:option usingIdentifier:(STIdentifier)identifier withBlock:block { return hook(self, sel, option, identifier, block);}+ st_hookClassMethod:sel option:option usingIdentifier:(STIdentifier)identifier withBlock:block { return hook(object_getClass, sel, option, identifier, block);}+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:key { NSMutableArray *mArray = [[NSMutableArray alloc] init]; @synchronized { [mArray addObjectsFromArray:getAllIdentifiers(self, key)]; [mArray addObjectsFromArray:getAllIdentifiers(object_getClass, key)]; } return [mArray copy];}+ st_removeHookWithIdentifier:(STIdentifier)identifier forKey:key { BOOL hasRemoved = NO; @synchronized { id<StingerInfoPool> infoPool = getStingerInfoPool(self, key); if ([infoPool removeInfoForIdentifier:identifier]) { hasRemoved = YES; } infoPool = getStingerInfoPool(object_getClass, key); if ([infoPool removeInfoForIdentifier:identifier]) { hasRemoved = YES; } } return hasRemoved;}#pragma - inline functionsNS_INLINE BOOL hook(Class cls, SEL sel, STOption option, STIdentifier identifier, id block) { NSCParameterAssert; NSCParameterAssert; NSCParameterAssert(option == 0 || option == 1 || option == 2); NSCParameterAssert(identifier); NSCParameterAssert; Method m = class_getInstanceMethod; NSCAssert(m, @"SEL  doesn't has a imp in Class  originally", NSStringFromSelector, cls); if  return NO; const char * typeEncoding = method_getTypeEncoding; STMethodSignature *methodSignature = [[STMethodSignature alloc] initWithObjCTypes:[NSString stringWithUTF8String:typeEncoding]]; STMethodSignature *blockSignature = [[STMethodSignature alloc] initWithObjCTypes:signatureForBlock]; if (! isMatched(methodSignature, blockSignature, option, cls, sel, identifier)) { return NO; } IMP originalImp = method_getImplementation; @synchronized { StingerInfo *info = [StingerInfo infoWithOption:option withIdentifier:identifier withBlock:block]; id<StingerInfoPool> infoPool = getStingerInfoPool; if  { return [infoPool addInfo:info]; } infoPool = [StingerInfoPool poolWithTypeEncoding:[NSString stringWithUTF8String:typeEncoding] originalIMP:originalImp selector:sel]; infoPool.cls = cls; IMP stingerIMP = [infoPool stingerIMP]; if (!(class_addMethod(cls, sel, stingerIMP, typeEncoding))) { class_replaceMethod(cls, sel, stingerIMP, typeEncoding); } const char * st_original_SelName = [[@"st_original_" stringByAppendingString:NSStringFromSelector] UTF8String]; class_addMethod(cls, sel_registerName(st_original_SelName), originalImp, typeEncoding); setStingerInfoPool(cls, sel, infoPool); return [infoPool addInfo:info]; }}NS_INLINE id<StingerInfoPool> getStingerInfoPool(Class cls, SEL key) { NSCParameterAssert; NSCParameterAssert; return objc_getAssociatedObject;}NS_INLINE void setStingerInfoPool(Class cls, SEL key, id<StingerInfoPool> infoPool) { NSCParameterAssert; NSCParameterAssert; objc_setAssociatedObject(cls, key, infoPool, OBJC_ASSOCIATION_RETAIN);}NS_INLINE NSArray<STIdentifier> * getAllIdentifiers(Class cls, SEL key) { NSCParameterAssert; NSCParameterAssert; id<StingerInfoPool> infoPool = getStingerInfoPool; return infoPool.identifiers;}NS_INLINE BOOL isMatched(STMethodSignature *methodSignature, STMethodSignature *blockSignature, STOption option, Class cls, SEL sel, NSString *identifier) { //argument count if (methodSignature.argumentTypes.count != blockSignature.argumentTypes.count) { NSCAssert(NO, @"count of arguments isn't equal. Class: , SEL: , Identifier: ", cls, NSStringFromSelector, identifier); return NO; }; // loc 1 should be id<StingerParams>. if (![blockSignature.argumentTypes[1] isEqualToString:@"@"]) { NSCAssert(NO, @"argument 1 should be object type. Class: , SEL: , Identifier: ", cls, NSStringFromSelector, identifier); return NO; } // from loc 2. for (NSInteger i = 2; i < methodSignature.argumentTypes.count; i++) { if (![blockSignature.argumentTypes[i] isEqualToString:methodSignature.argumentTypes[i]]) { NSCAssert(NO, @"argument  type isn't equal. Class: , SEL: , Identifier: ", i, cls, NSStringFromSelector, identifier); return NO; } } // when STOptionInstead, returnType if (option == STOptionInstead && ![blockSignature.returnType isEqualToString:methodSignature.returnType]) { NSCAssert(NO, @"return type isn't equal. Class: , SEL: , Identifier: ", cls, NSStringFromSelector, identifier); return NO; } return YES;}@end

import UIKit;@interface ASViewController : UIViewController- print1:(NSString *)s;- (NSString *)print2:(NSString *)s;@end

#import "ASViewController+hook.h"@implementation ASViewController + load { /* * hook @selector */ [self st_hookInstanceMethod:@selector option:STOptionBefore usingIdentifier:@"hook_print1_before1" withBlock:^(id<StingerParams> params, NSString *s) { NSLog(@"---before1 print1: %@", s); }]; [self st_hookInstanceMethod:@selector option:STOptionBefore usingIdentifier:@"hook_print1_before2" withBlock:^(id<StingerParams> params, NSString *s) { NSLog(@"---before2 print1: %@", s); }]; [self st_hookInstanceMethod:@selector option:STOptionAfter usingIdentifier:@"hook_print1_after1" withBlock:^(id<StingerParams> params, NSString *s) { NSLog(@"---after1 print1: %@", s); }]; [self st_hookInstanceMethod:@selector option:STOptionAfter usingIdentifier:@"hook_print1_after2" withBlock:^(id<StingerParams> params, NSString *s) { NSLog(@"---after2 print1: %@", s); }]; /* * hook @selector */ __block NSString *oldRet, *newRet; [self st_hookInstanceMethod:@selector option:STOptionInstead usingIdentifier:@"hook_print2_instead" withBlock:^NSString * (id<StingerParams> params, NSString *s) { [params invokeAndGetOriginalRetValue:&oldRet]; newRet = [oldRet stringByAppendingString:@" ++ new-st_instead"]; NSLog(@"---instead print2 old ret:  / new ret: ", oldRet, newRet); return newRet; }]; [self st_hookInstanceMethod:@selector option:STOptionAfter usingIdentifier:@"hook_print2_after1" withBlock:^(id<StingerParams> params, NSString *s) { NSLog(@"---after1 print2 self:%@ SEL: %@ p: %@",[params slf], NSStringFromSelector([params sel]), s); }];}@end

Stinger用法与Aspects很相似,但收到消息后,由于block和原始IMP直接使用函数指针进行调用,不处理额外的消息,不用实例化诸多NSInvocation对象,两个lib_cif对象在hook后也即准备好,相比aspects,实测有5%到50%左右的速度提升。使用其他方式hook时,仍能保证st_hook的有效性。

-(void)forwardInvocation:(NSInvocation*)invocation

谢谢观看,水平有限,如有错误,请指正。

图片 15Stinger-2.jpg

 NSInvocation把尚未处理的那条消息有关的全部细节都封于其中,包括:选择子、目标及参数。

参考资料

;

(a)上面这个方法可以实现的很简单:只需改变调用目标,使消息在新目标上得以调用即可(与“备援接收者”方案所实现的方法等效,很少有人采用)。

(b)比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等。

上面的步骤都不能解决问题的话,就会调用NSObject的doesNotRecognizeSelector抛出异常。

总结:

  消息转发的全流程,如下图所示:

图片 16

“消息转发”全流程图

二、举例

1. 动态方法解析,即resolveInstanceMethod的使用:

  (以动态方法解析来实现@dynamic属性)

//EOCAutoDictionary.h

@interface EOCAutoDictionary : NSObject

@property(nonatomic, strong) NSDate *date;

@end

//EOCAutoDictionary.m

#import "EOCAutoDictionary.h"

@interface EOCAutoDictionary()

@property(nonatomic, strong) NSMutableDictionary *backingStore;

@end

@implementation EOCAutoDictionary

@dynamic date;

- (id)init {

    if(self = [super init]) {_backingStore = [NSMutableDictionary new];}

    return self;

}

+ (BOOL) resolveInstanceMethod:(SEL)selector {

    //selector = "setDate:" 或 "date",_cmd = (SEL)"resolveInstanceMethod:"

    NSString *selectorString = NSStringFromSelector(selector);

    if([selectorString hasPrefix:@"set"]) {

本文由必威发布于必威-编程,转载请注明出处:它会在运行时通过目标对象去寻找对应的方法,

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。