Hook方法的新姿势--Stinger (使用libffi实现AOP )

前言

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

@interface ClassA: NSObject
- (void)methodA;
+ (void)methodB;
@end

...

@implementation ClassA (Swizzle)

+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(methodA));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_methodA));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)swizzled_methodA {
    ...
    [self swizzled_methodA];
    ...
}

@end

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

// hook instance method
[ClassA aspect_hookSelector:@selector(methodA)
                      withOptions:AspectPositionAfter
                       usingBlock:^{...}
                            error:nil];
  
// hook class method
[object_getClass(ClassA) aspect_hookSelector:@selector(methodB)
                      withOptions:AspectPositionAfter
                       usingBlock:^{...}
                            error:nil];

    另外,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 可以帮我们做到这一切。

1. libffi 简介

    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>

1.1 ffi_type

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

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;

1.2 ffi_call

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

void **args = malloc(sizeof(void *) * 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); 
// 动态调用fun1
ffi_call(&cif, fun1,  &ret, args);
...

// 输出: ret = 3;

1.3 ffi_prep_closure_loc

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

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 *)(self), imp);

void *imp = NULL;
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&imp);

//生成ffi_closure
ffi_prep_closure_loc(closure, &cif, ffi_function, (__bridge void *)(self), stingerIMP);

    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>

#import <Foundation/Foundation.h>
#import "StingerParams.h"

typedef NSString *STIdentifier;

typedef NS_ENUM(NSInteger, STOption) {
  STOptionAfter = 0,
  STOptionInstead = 1,
  STOptionBefore = 2,
};

@interface NSObject (Stinger)

+ (BOOL)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;

+ (BOOL)st_hookClassMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;

+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:(SEL)key;

+ (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key;

@end

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

2 方法签名 & ffi_type

2.1 方法签名 -> ffi_type

    对于方法的签名和type encoding,笔者在 用代码理解ObjC中的发送消息和消息转发 一文中已经有了不少介绍。简而言之,type encoding 字符串与方法的返回类型及参数类型是一一对应的。例如:- (void)print1:(NSString *)s;的type encoding为v24@0:8@16v对应void@对应id(这里是self),:对应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(@?为block),得到参数类型数组(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;
}

2.2 浅谈block

2.2.1 签名 & 函数指针

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

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

enum {
  BLOCK_DEALLOCATING =      (0x0001),  // runtime
  BLOCK_REFCOUNT_MASK =     (0xfffe),  // 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 1
struct Block_descriptor_1 {
  unsigned long int reserved;
  unsigned long int size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
  // requires BLOCK_HAS_COPY_DISPOSE
  void (*copy)(void *dst, const void *src);
  void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct 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 (*invoke)(void *, ...);
  struct Block_descriptor_1 *descriptor;
  // imported variables
};

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

NSString *signatureForBlock(id block) {
  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(void *);
  
  if (!descRef)
    return nil;
  
  const char *signature = (*(const char **)descRef);
  return [NSString stringWithUTF8String:signature];
}

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

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

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

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

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

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

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

此外,实测通过IMP _Nonnull imp_implementationWithBlock(id _Nonnull block)获得的函数指针对应的参数并不包含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)blockIMP;

NSString *signatureForBlock(id block);
BlockIMP impForBlock(id block);
@end
#define NSBlock NSClassFromString(@"NSBlock")

void addInstanceMethodForBlock(SEL sel) {
  Method m = class_getInstanceMethod(STBlock.class, sel);
  if (!m) return;
  IMP imp = method_getImplementation(m);
  const char *typeEncoding = method_getTypeEncoding(m);
  class_addMethod(NSBlock, sel, imp, typeEncoding);
}

@implementation STBlock

+ (void)load {
  addInstanceMethodForBlock(@selector(signature));
  addInstanceMethodForBlock(@selector(blockIMP));
}

...

@end

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

3 存储hook信息 & 生成两个ffi_cif对象

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:(STOption)option withIdentifier:(STIdentifier)identifier withBlock:(id)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;
- (BOOL)addInfo:(id<StingerInfo>)info;
- (BOOL)removeInfoForIdentifier:(STIdentifier)identifier;

@optional
@property (nonatomic, weak) Class cls;
+ (instancetype)poolWithTypeEncoding:(NSString *)typeEncoding originalIMP:(IMP)imp selector:(SEL)sel;
@end

@interface StingerInfoPool : NSObject <StingerInfoPool>
@end

3.2.1 管理StingerInfo

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

3.2.2 生成方法调用模板 cif

    根据原始方法提供的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), (void **)&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 *)(self), stingerIMP) != FFI_OK) {
      NSAssert(NO, @"genarate IMP failed");
    }
  } else {
    NSAssert(NO, @"FUCK");
  }
  
  [self _genarateBlockCif];
  return stingerIMP;
}

3.2.3 生成block调用模板 blockCif

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

- (void)_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,后面接的是与原方法对应的参数。

3.2.4 ffi_function !!!

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

- (void)invokeAndGetOriginalRetValue:(void *)retLoc;
@end

@interface StingerParams : NSObject <StingerParams>
- (void)addOriginalInvocation:(NSInvocation *)invocation;
- (void)addOriginalIMP:(IMP)imp;
@end

4 替换方法实现 & 记录HOOK

    思路是对某个类以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 (Stinger)

#pragma - public

+ (BOOL)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block {
  return hook(self, sel, option, identifier, block);
}

+ (BOOL)st_hookClassMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block {
  return hook(object_getClass(self), sel, option, identifier, block);
}

+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:(SEL)key {
  NSMutableArray *mArray = [[NSMutableArray alloc] init];
  @synchronized(self) {
    [mArray addObjectsFromArray:getAllIdentifiers(self, key)];
    [mArray addObjectsFromArray:getAllIdentifiers(object_getClass(self), key)];
  }
  return [mArray copy];
}

+ (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key {
  BOOL hasRemoved = NO;
  @synchronized(self) {
    id<StingerInfoPool> infoPool = getStingerInfoPool(self, key);
    if ([infoPool removeInfoForIdentifier:identifier]) {
      hasRemoved = YES;
    }
    infoPool = getStingerInfoPool(object_getClass(self), key);
    if ([infoPool removeInfoForIdentifier:identifier]) {
      hasRemoved = YES;
    }
  }
  return hasRemoved;
}

#pragma - inline functions

NS_INLINE BOOL hook(Class cls, SEL sel, STOption option, STIdentifier identifier, id block) {
  NSCParameterAssert(cls);
  NSCParameterAssert(sel);
  NSCParameterAssert(option == 0 || option == 1 || option == 2);
  NSCParameterAssert(identifier);
  NSCParameterAssert(block);
  Method m = class_getInstanceMethod(cls, sel);
  NSCAssert(m, @"SEL (%@) doesn't has a imp in Class (%@) originally", NSStringFromSelector(sel), cls);
  if (!m) return NO;
  const char * typeEncoding = method_getTypeEncoding(m);
  STMethodSignature *methodSignature = [[STMethodSignature alloc] initWithObjCTypes:[NSString stringWithUTF8String:typeEncoding]];
  STMethodSignature *blockSignature = [[STMethodSignature alloc] initWithObjCTypes:signatureForBlock(block)];
  if (! isMatched(methodSignature, blockSignature, option, cls, sel, identifier)) {
    return NO;
  }

  IMP originalImp = method_getImplementation(m);
  
  @synchronized(cls) {
    StingerInfo *info = [StingerInfo infoWithOption:option withIdentifier:identifier withBlock:block];
    id<StingerInfoPool> infoPool = getStingerInfoPool(cls, sel);
    
    if (infoPool) {
      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(sel)] 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(cls);
  NSCParameterAssert(key);
  return objc_getAssociatedObject(cls, key);
}

NS_INLINE void setStingerInfoPool(Class cls, SEL key, id<StingerInfoPool> infoPool) {
  NSCParameterAssert(cls);
  NSCParameterAssert(key);
  objc_setAssociatedObject(cls, key, infoPool, OBJC_ASSOCIATION_RETAIN);
}

NS_INLINE NSArray<STIdentifier> * getAllIdentifiers(Class cls, SEL key) {
  NSCParameterAssert(cls);
  NSCParameterAssert(key);
  id<StingerInfoPool> infoPool = getStingerInfoPool(cls, key);
  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(sel), 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(sel), 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 (%zd) type isn't equal. Class: (%@), SEL: (%@), Identifier: (%@)", i, cls, NSStringFromSelector(sel), 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(sel), identifier);
    return NO;
  }
  
  return YES;
}

@end

* 使用示例

import UIKit;

@interface ASViewController : UIViewController

- (void)print1:(NSString *)s;

- (NSString *)print2:(NSString *)s;

@end
#import "ASViewController+hook.h"

@implementation ASViewController (hook)

+ (void)load {
  /*
   * hook @selector(print1:)
   */
  [self st_hookInstanceMethod:@selector(print1:) option:STOptionBefore usingIdentifier:@"hook_print1_before1" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---before1 print1: %@", s);
  }];
  
  [self st_hookInstanceMethod:@selector(print1:) option:STOptionBefore usingIdentifier:@"hook_print1_before2" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---before2 print1: %@", s);
  }];
  
  [self st_hookInstanceMethod:@selector(print1:) option:STOptionAfter usingIdentifier:@"hook_print1_after1" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---after1 print1: %@", s);
  }];
  
  [self st_hookInstanceMethod:@selector(print1:) option:STOptionAfter usingIdentifier:@"hook_print1_after2" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---after2 print1: %@", s);
  }];
  
  /*
   * hook @selector(print2:)
   */
  __block NSString *oldRet, *newRet;
  [self st_hookInstanceMethod:@selector(print2:) 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(print2:) 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的有效性。

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

https://github.com/Assuner-Lee/Stinger

Stinger-2.jpg

参考资料

https://github.com/opensource-apple/objc4</br>
http://blog.cnbang.net/tech/3219/</br>
https://juejin.im/post/5a308f856fb9a0452b4937cc</br>
https://github.com/mikeash/MABlockClosure</br>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,764评论 0 38
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,182评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,548评论 33 466
  • 谁让你平静的心灵湖面扔了石子 谁让你平静的内心起了涟漪 何时的你有了浮躁 何种境地让你开始质疑自己? ...
    姚小妖精阅读 524评论 0 6