Hook方法的新姿勢--Stinger (使用libffi實現AOP )

Assuner發表於2018-01-19

前言

    眾所周知,使用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。
    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中的傳送訊息和訊息轉發
    Aspects 實現程式碼裡的很多細節處理是很令人稱道的,且支援hook類的單個例項物件的方法(類似於KVO的isa-swizzlling)。但由於對原方法呼叫直接進行了訊息轉發,到真正的IMP對應的函式被執行前,經歷了對其他多個訊息的處理,invoke block也需要額外的invocation構建開銷。作者也在註釋中寫道,不適合對每秒鐘超過1000次的方法增加切面程式碼。此外,使用其他方式對Aspect hook過的方法進行hook時,如直接替換為新的IMP,新hook得到的原始實現是_objc_msgForward,之前的aspect_hook會失效,新的hook也將執行異常。
    那麼不禁要思考,有沒有一種方式可以替換原方法的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。當然,在這個函式裡我們可以做一些額外的操作。

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 函式
    到這裡,對於如何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。

#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
    此外,我們可以直接獲取到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位開始複製原始的引數。
    以下是完成切面程式碼和原始imp執行的過程:
    1. 利用ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs);完成所有切面位置在前block的呼叫。使用block模板blockCif和innerArgs。
    2. 利用ffi_call(cif, (void (*)(void))self.originalIMP / impForBlock(block), ret, args);完成對原始IMP或替換位置block imp的呼叫。使用原始模板cif和原始引數args,並可能產生返回值。
    3. 利用ffi_call(&(self->_blockCif), impForBlock(block), NULL, innerArgs);完成所有切面位置在後的block的呼叫。使用block模板blockCif和innerArgs。

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。

#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>物件中。
    關於條件,最主要的就是兩點,第一點就是對於某個類中(父類)的某個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,實測從收到訊息到執行到切面資訊(例如before block)這個過程, 花費時間降低一個數量級。使用其他方式hook時,仍能保證st_hook的有效性。

謝謝觀看,水平有限,如有錯誤,請指正。

github.com/Assuner-Lee…

Hook方法的新姿勢--Stinger (使用libffi實現AOP )

參考資料

github.com/opensource-…
blog.cnbang.net/tech/3219/
juejin.im/post/5a308f…
github.com/mikeash/MAB…

相關文章